Ver2.0 Retrofit: 타입-세이프 HTTP 클라이언트로 안드로이드 앱 개발 레벨업하기 🚀

Retrofit: 타입-세이프 HTTP 클라이언트로 안드로이드 앱 개발 레벨업하기 🚀
안녕하세요 여러분! 오늘은 안드로이드 개발자들 사이에서 절대 빼놓을 수 없는 필수 라이브러리 중 하나인 Retrofit에 대해 함께 알아볼게요! 🤓 서버와 통신하는 코드 작성할 때 매번 힘들었다면, 이 글이 여러분의 개발 인생을 바꿔줄지도...? ㅋㅋㅋ
Java나 Kotlin으로 안드로이드 앱 개발하시는 분들이라면 한 번쯤은 들어봤을 Retrofit! 이 강력한 HTTP 클라이언트 라이브러리가 어떻게 여러분의 코드를 더 안전하고 깔끔하게 만들어주는지 함께 파헤쳐 봅시다! 😎
Retrofit이 뭐길래? 🤔
Retrofit은 Square사에서 개발한 타입-세이프한 HTTP 클라이언트예요. 뭔가 어려워 보이죠? 쉽게 말하면 "서버와 통신할 때 실수 없이 안전하게 데이터를 주고받을 수 있게 도와주는 라이브러리"라고 생각하시면 됩니다!
여기서 '타입-세이프(Type-Safe)'란 무슨 뜻일까요? 간단히 말해서, 컴파일 시점에 타입 오류를 잡아준다는 의미예요. 즉, 코드 작성할 때 실수를 미리 발견해서 런타임 에러를 방지해준다는 거죠! 진짜 개발자의 수명 연장제 같은 존재...ㅋㅋㅋ 😂
Retrofit의 주요 특징 ✨
- 인터페이스 기반 API 정의 - 메서드 선언만으로 HTTP 요청을 정의할 수 있어요
- 강력한 타입 안전성 - Java/Kotlin의 타입 시스템을 활용해 API 응답을 객체로 자동 변환
- 유연한 확장성 - 다양한 컨버터와 어댑터를 지원해 JSON, XML 등 다양한 형식 처리 가능
- 간결한 코드 - 복잡한 네트워크 로직을 단순화해 코드 가독성 향상
- 비동기 처리 지원 - RxJava, Coroutines 등과 함께 사용하기 좋음
Retrofit vs 전통적인 HTTP 통신 비교 🥊
전통적인 방식 (HttpURLConnection)
URL url = new URL("https://api.example.com/users");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// JSON 파싱 작업 필요...
// 에러 처리 필요...
// 타입 변환 작업 필요...
}
connection.disconnect();
Retrofit 방식
// 인터페이스 정의
public interface UserService {
@GET("users")
Call<List<User>> getUsers();
}
// 사용
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
UserService service = retrofit.create(UserService.class);
Call<List<User>> call = service.getUsers();
call.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
List<User> users = response.body(); // 자동으로 객체 변환!
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
// 에러 처리
}
});
확실히 Retrofit이 코드가 더 깔끔하고 직관적이죠? 😮
Retrofit 시작하기 🚀
자, 이제 Retrofit을 실제로 프로젝트에 적용해볼까요? 먼저 의존성부터 추가해야겠죠!
1. 의존성 추가하기
Gradle 파일에 다음 코드를 추가해주세요:
// build.gradle (모듈 수준)
dependencies {
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// JSON 변환기 (Gson)
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 선택사항: 로깅 인터셉터
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
}
2. 모델 클래스 만들기
서버에서 받아올 데이터 구조에 맞게 모델 클래스를 정의해줍니다:
public class User {
private int id;
private String name;
private String email;
// 게터와 세터 메서드
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
3. API 인터페이스 정의하기
이제 진짜 Retrofit의 마법이 시작되는 부분이에요! API 호출을 위한 인터페이스를 정의합니다:
public interface ApiService {
// 사용자 목록 가져오기
@GET("users")
Call<List<User>> getUsers();
// 특정 사용자 정보 가져오기
@GET("users/{id}")
Call<User> getUserById(@Path("id") int userId);
// 새 사용자 추가하기
@POST("users")
Call<User> createUser(@Body User user);
// 사용자 정보 업데이트
@PUT("users/{id}")
Call<User> updateUser(@Path("id") int userId, @Body User user);
// 사용자 삭제
@DELETE("users/{id}")
Call<Void> deleteUser(@Path("id") int userId);
}
어때요? 메서드 선언만으로 HTTP 요청의 모든 것을 정의할 수 있어요! 진짜 신기하지 않나요?! 😲
4. Retrofit 인스턴스 생성하기
이제 실제로 Retrofit 객체를 만들어 API를 호출해봅시다:
// OkHttp 클라이언트 (로깅 추가)
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.build();
// Retrofit 빌더
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/") // 기본 URL 설정
.client(client) // OkHttp 클라이언트 설정
.addConverterFactory(GsonConverterFactory.create()) // JSON 변환기 설정
.build();
// API 서비스 생성
ApiService apiService = retrofit.create(ApiService.class);
5. API 호출하기
드디어 API를 호출해볼 차례입니다!
// 비동기 호출
Call<List<User>> call = apiService.getUsers();
call.enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if (response.isSuccessful()) {
List<User> users = response.body();
// 데이터 처리
for (User user : users) {
Log.d("Retrofit", "User: " + user.getName());
}
} else {
Log.e("Retrofit", "Error: " + response.code());
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
Log.e("Retrofit", "Network Error: " + t.getMessage());
}
});
와! 이렇게 간단하게 서버와 통신할 수 있다니! 이제 네트워크 통신 코드 때문에 머리 아플 일이 없겠죠? ㅎㅎ
Retrofit 고급 기능 알아보기 🧙♂️
기본적인 사용법을 익혔다면, 이제 Retrofit의 더 강력한 기능들을 살펴볼까요? 이 부분에서 여러분의 개발 스킬이 한층 업그레이드될 거예요! 💪
1. 쿼리 파라미터 사용하기
URL에 쿼리 파라미터를 추가하는 방법은 아주 간단해요:
@GET("users")
Call<List<User>> getUsersByRole(@Query("role") String role);
이렇게 하면 https://api.example.com/users?role=admin 같은 URL로 요청이 전송돼요.
2. 여러 쿼리 파라미터 한번에 전송하기
여러 쿼리 파라미터를 Map으로 한번에 전송할 수도 있어요:
@GET("users")
Call<List<User>> getUsersWithFilters(@QueryMap Map<String, String> filters);
3. 헤더 추가하기
API 요청에 헤더를 추가하는 방법도 있어요:
// 정적 헤더
@Headers({
"Accept: application/json",
"User-Agent: Retrofit-Sample-App"
})
@GET("users")
Call<List<User>> getUsers();
// 동적 헤더
@GET("users")
Call<List<User>> getUsersWithAuth(@Header("Authorization") String authToken);
4. 멀티파트 요청 (파일 업로드)
이미지나 파일을 업로드할 때는 멀티파트 요청을 사용해요:
@Multipart
@POST("upload")
Call<ResponseBody> uploadImage(
@Part("description") RequestBody description,
@Part MultipartBody.Part file
);
// 사용 예시
File file = new File(imagePath);
RequestBody requestFile = RequestBody.create(MediaType.parse("image/jpeg"), file);
MultipartBody.Part body = MultipartBody.Part.createFormData("picture", file.getName(), requestFile);
RequestBody description = RequestBody.create(MediaType.parse("text/plain"), "이미지 설명");
Call<ResponseBody> call = apiService.uploadImage(description, body);
5. 인터셉터 활용하기
인터셉터는 모든 요청에 공통 로직을 적용할 때 정말 유용해요. 예를 들어, 모든 요청에 인증 토큰을 추가하고 싶다면:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request original = chain.request();
// 헤더 추가
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getAuthToken())
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
})
.build();
6. 커스텀 컨버터 사용하기
기본적으로 Gson을 많이 사용하지만, Moshi나 Jackson 같은 다른 JSON 파서도 사용할 수 있어요:
// Moshi 사용
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create())
.build();
// Jackson 사용
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(JacksonConverterFactory.create())
.build();
이런 고급 기능들을 활용하면 더 복잡한 API 통신도 쉽게 처리할 수 있어요. 재능넷에서 프로젝트를 의뢰하실 때도 이런 기술들을 요구사항에 포함시키면 더 효율적인 앱을 개발할 수 있을 거예요! 😉
Retrofit 사용 시 꿀팁 모음 🍯
- Base URL은 항상 슬래시(/)로 끝나야 해요 - 그렇지 않으면 예상치 못한 URL이 생성될 수 있어요.
- 응답 처리는 메인 스레드에서 이루어져요 - UI 업데이트는 바로 할 수 있지만, 무거운 작업은 별도 스레드로 처리하세요.
- 에러 응답도 객체로 파싱할 수 있어요 -
response.errorBody()를 사용해 에러 응답을 처리하세요. - 동기 호출도 가능해요 -
call.execute()를 사용하면 되지만, 반드시 백그라운드 스레드에서 호출해야 해요. - Call 객체는 한 번만 실행할 수 있어요 - 재사용하려면
call.clone()을 사용하세요. - 타임아웃 설정을 잊지 마세요 - OkHttpClient에서 설정할 수 있어요.
- ProGuard 사용 시 주의하세요 - 모델 클래스와 인터페이스가 난독화되지 않도록 규칙을 추가해야 해요.
이런 팁들을 기억하면 Retrofit으로 인한 대부분의 문제를 피할 수 있어요!
Retrofit과 RxJava 함께 사용하기 🔄
Retrofit은 RxJava와 함께 사용하면 더욱 강력해져요! 비동기 처리와 데이터 스트림 관리가 훨씬 쉬워진답니다.
1. 의존성 추가하기
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.19'
2. Retrofit 설정하기
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // RxJava 어댑터 추가
.build();
3. API 인터페이스 정의하기
public interface ApiService {
@GET("users")
Observable<List<User>> getUsers();
@GET("users/{id}")
Single<User> getUserById(@Path("id") int userId);
@POST("users")
Completable createUser(@Body User user);
}
4. API 호출하기
apiService.getUsers()
.subscribeOn(Schedulers.io()) // IO 스레드에서 네트워크 작업 실행
.observeOn(AndroidSchedulers.mainThread()) // 메인 스레드에서 결과 처리
.subscribe(new Observer<List<User>>() {
@Override
public void onSubscribe(Disposable d) {
// 구독 시작 시 호출
}
@Override
public void onNext(List<User> users) {
// 데이터 수신 시 호출
}
@Override
public void onError(Throwable e) {
// 에러 발생 시 호출
}
@Override
public void onComplete() {
// 완료 시 호출
}
});
RxJava를 사용하면 여러 API 호출을 조합하거나 변환하는 작업이 정말 쉬워져요. 예를 들어, 사용자 정보를 가져온 후 해당 사용자의 게시물을 가져오는 작업을 체이닝할 수 있어요:
apiService.getUserById(1)
.flatMap(user -> {
// 사용자 정보 처리
Log.d("User", user.getName());
// 해당 사용자의 게시물 가져오기
return apiService.getUserPosts(user.getId());
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(posts -> {
// 게시물 처리
}, throwable -> {
// 에러 처리
});
진짜 짱 편하지 않나요?! ㅋㅋㅋ 이런 기능 덕분에 복잡한 비동기 로직도 깔끔하게 처리할 수 있어요. 😎
Retrofit과 Kotlin Coroutines 함께 사용하기 ⚡
Kotlin 개발자라면 Coroutines와 Retrofit의 조합은 정말 환상적이에요! 비동기 코드를 마치 동기 코드처럼 작성할 수 있어 가독성이 크게 향상돼요.
1. 의존성 추가하기
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
2. API 인터페이스 정의하기
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
@GET("users/{id}")
suspend fun getUserById(@Path("id") userId: Int): User
@POST("users")
suspend fun createUser(@Body user: User): User
}
보이시나요? suspend 키워드만 추가하면 코루틴에서 바로 사용할 수 있어요! 별도의 Call 래퍼 없이 직접 데이터 타입을 반환할 수 있답니다.
3. API 호출하기
// ViewModel 내부
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> = _users
private val viewModelJob = SupervisorJob()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun loadUsers() {
viewModelScope.launch {
try {
// withContext를 사용해 IO 스레드에서 네트워크 작업 실행
val result = withContext(Dispatchers.IO) {
apiService.getUsers()
}
_users.value = result
} catch (e: Exception) {
// 에러 처리
}
}
}
코루틴을 사용하면 try-catch 블록으로 예외 처리도 쉽게 할 수 있어요. 또한 여러 API 호출을 순차적으로 하거나 병렬로 처리하는 것도 간단해요:
// 순차적 API 호출
viewModelScope.launch {
try {
val user = withContext(Dispatchers.IO) {
apiService.getUserById(1)
}
val posts = withContext(Dispatchers.IO) {
apiService.getUserPosts(user.id)
}
// 두 결과 모두 처리
} catch (e: Exception) {
// 에러 처리
}
}
// 병렬 API 호출
viewModelScope.launch {
try {
val deferred1 = async(Dispatchers.IO) { apiService.getUsers() }
val deferred2 = async(Dispatchers.IO) { apiService.getPosts() }
val users = deferred1.await()
val posts = deferred2.await()
// 두 결과 모두 처리
} catch (e: Exception) {
// 에러 처리
}
}
코루틴과 Retrofit의 조합은 안드로이드 개발자의 삶의 질을 확실히 높여주는 기술이에요! 재능넷에서 안드로이드 개발자를 찾고 계신다면, 코루틴과 Retrofit에 능숙한 개발자를 찾는 것이 좋을 것 같네요! 😉
실전 예제: 날씨 앱 API 연동하기 🌤️
지금까지 배운 내용을 토대로 실제 날씨 API를 연동하는 예제를 만들어볼게요! OpenWeatherMap API를 사용해볼 거예요.
1. 모델 클래스 정의하기
// 날씨 응답 모델
public class WeatherResponse {
private Main main;
private List<Weather> weather;
private Wind wind;
private String name;
// 게터와 세터
public Main getMain() { return main; }
public List<Weather> getWeather() { return weather; }
public Wind getWind() { return wind; }
public String getName() { return name; }
// 내부 클래스들
public static class Main {
private double temp;
private double feels_like;
private double humidity;
public double getTemp() { return temp; }
public double getFeelsLike() { return feels_like; }
public double getHumidity() { return humidity; }
}
public static class Weather {
private String main;
private String description;
private String icon;
public String getMain() { return main; }
public String getDescription() { return description; }
public String getIcon() { return icon; }
}
public static class Wind {
private double speed;
public double getSpeed() { return speed; }
}
}
2. API 인터페이스 정의하기
public interface WeatherApiService {
@GET("weather")
Call<WeatherResponse> getCurrentWeather(
@Query("q") String city,
@Query("units") String units,
@Query("appid") String apiKey
);
}
3. Retrofit 인스턴스 생성하기
private static final String BASE_URL = "https://api.openweathermap.org/data/2.5/";
private static final String API_KEY = "your_api_key_here";
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
WeatherApiService weatherApiService = retrofit.create(WeatherApiService.class);
4. 날씨 데이터 가져오기
Call<WeatherResponse> call = weatherApiService.getCurrentWeather("Seoul", "metric", API_KEY);
call.enqueue(new Callback<WeatherResponse>() {
@Override
public void onResponse(Call<WeatherResponse> call, Response<WeatherResponse> response) {
if (response.isSuccessful()) {
WeatherResponse weatherData = response.body();
// 데이터 표시
String cityName = weatherData.getName();
double temperature = weatherData.getMain().getTemp();
String weatherDesc = weatherData.getWeather().get(0).getDescription();
textViewCity.setText(cityName);
textViewTemp.setText(temperature + "°C");
textViewDesc.setText(weatherDesc);
// 아이콘 로딩 (별도 이미지 로더 사용)
String iconCode = weatherData.getWeather().get(0).getIcon();
String iconUrl = "https://openweathermap.org/img/w/" + iconCode + ".png";
// 이미지 로더로 iconUrl 로딩
} else {
// 에러 처리
Log.e("Weather", "Error: " + response.code());
}
}
@Override
public void onFailure(Call<WeatherResponse> call, Throwable t) {
Log.e("Weather", "Network Error: " + t.getMessage());
}
});
이렇게 하면 실제 날씨 API를 연동한 앱을 만들 수 있어요! 정말 간단하지 않나요? Retrofit이 없었다면 훨씬 더 많은 코드를 작성해야 했을 거예요. 😱
이런 식으로 다양한 API를 연동해서 멋진 앱을 만들 수 있어요. 재능넷에서 앱 개발 프로젝트를 진행하실 때도 Retrofit을 활용하면 개발 시간을 크게 단축할 수 있을 거예요! 🚀
자주 발생하는 문제와 해결책 🔧
Retrofit을 사용하다 보면 몇 가지 흔한 문제에 부딪힐 수 있어요. 미리 알아두면 시간을 절약할 수 있겠죠?
1. CLEARTEXT 통신 문제
문제: Android 9(Pie) 이상에서 HTTP(HTTPS가 아닌) 요청 시 "Cleartext HTTP traffic not permitted" 에러가 발생해요.
해결책: AndroidManifest.xml에 다음 설정을 추가하세요:
<application
...
android:usesCleartextTraffic="true"
... >
하지만 보안을 위해 가능하면 HTTPS를 사용하는 것이 좋아요!
2. JSON 파싱 오류
문제: "Expected BEGIN_ARRAY but was BEGIN_OBJECT" 같은 JSON 파싱 에러가 발생해요.
해결책: 모델 클래스가 실제 JSON 응답 구조와 일치하는지 확인하세요. JSON 응답을 로그로 출력해보고 모델 클래스를 수정하세요.
3. 네트워크 권한 누락
문제: 앱이 인터넷에 접속할 수 없어요.
해결책: AndroidManifest.xml에 인터넷 권한을 추가하세요:
<uses-permission android:name="android.permission.INTERNET" />
4. 타임아웃 문제
문제: 네트워크 요청이 너무 오래 걸리거나 타임아웃 돼요.
해결책: OkHttpClient에 타임아웃 설정을 추가하세요:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
5. 메인 스레드에서의 네트워크 호출
문제: "NetworkOnMainThreadException" 에러가 발생해요.
해결책: 동기 호출(execute())은 반드시 백그라운드 스레드에서 실행하세요. 비동기 호출(enqueue())을 사용하거나, 코루틴의 Dispatchers.IO를 사용하세요.
이런 문제들은 Retrofit을 사용하는 많은 개발자들이 한 번쯤 겪는 일반적인 문제들이에요. 미리 알아두면 디버깅 시간을 크게 줄일 수 있답니다! 😊
마무리: Retrofit으로 개발 생산성 높이기 🚀
지금까지 Retrofit의 기본부터 고급 기능까지 살펴봤어요. Retrofit은 안드로이드 개발자의 필수 도구라고 해도 과언이 아니에요! 서버 통신 코드를 간결하게 작성할 수 있고, 타입 안전성을 보장해주며, 다양한 확장 기능을 제공하기 때문이죠.
특히 RxJava나 Kotlin Coroutines와 함께 사용하면 비동기 처리도 정말 쉬워져요. 복잡한 네트워크 로직도 몇 줄의 코드로 구현할 수 있답니다!
앱 개발에서 서버 통신은 빠질 수 없는 부분이에요. 재능넷에서 앱 개발 프로젝트를 의뢰하거나 수주할 때도 Retrofit 같은 현대적인 라이브러리 활용 능력은 큰 경쟁력이 될 수 있어요. 더 효율적이고 안정적인 앱을 만들 수 있으니까요! 💪
이 글이 여러분의 Retrofit 이해에 도움이 되었길 바라요. 더 많은 개발 관련 정보나 프로젝트 의뢰는 재능넷(https://www.jaenung.net)을 통해 만나보세요! 다양한 개발자들과 함께 멋진 프로젝트를 진행할 수 있을 거예요. 😊
Happy Coding! 코드 한 줄 한 줄이 여러분의 앱을 더 멋지게 만들어갑니다! ✨
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.