밤빵's 개발일지
[TIL]20240912 리플렉션(Reflection)과 자바철학 본문
🫨리플렉션(Reflection)
결제 기능을 구현하는 과정에서 포트원(Port One)과 연동하면서 처음에는 레스트템플릿(RestTemplate)을 사용하고있다.하지만 포트원라이브러리를 이용하기위해 아이엠포트(Iamport) 클라이언트를 사용하려고 코드 수정을 시도하면서 몇 가지 문제가 발생했다. 아이엠포트의 Prepare 객체에서 데이터를 가져와야 했지만, 그 데이터가 읽기 전용(read-only) 필드로 설정되어 있어 getAmount()와 같은 getter 메서드를 사용할 수 없었다. 이 문제를 해결하기 위해 리플렉션(Reflection)이라는 방법을 알게되었는데... 아무리 봐도 이 방법은 쓰고 싶지가 않다😢
▶리플렉션이란?
리플렉션(Reflection)은 자바에서 클래스, 메서드, 필드 등의 정보를 실행 시간에 동적으로 조사하거나 수정할 수 있는 기능으로 주로 객체의 필드나 메서드에 접근할 수 없는 경우, 이를 강제로 접근하기 위해 사용된다. 이번 경우처럼 아이엠포트 클라이언트의 Prepare 객체가 내부적으로 읽기 전용 필드로 선언된 경우, 직접 필드에 접근할 수 없기 때문에 리플렉션을 통해 강제로 필드 값을 가져오는 방법이다.
▶문제 상황
아이엠포트 클라이언트에서 Prepare 객체의 amount 필드에 접근하려고 했으나, 이 필드는 읽기 전용으로 설정되어 있었고 getAmount() 메서드가 정의되어 있지 않았기 때문에 바로 데이터를 가져올 수 없었다. 스크린샷에서도 볼 수 있듯이, 컴파일러는 getAmount() 메서드를 찾지 못해 에러가 발생했다. 리플렉션을 사용한다면 amount 필드에 직접 접근할 수 있다.
▶리플렉션을 사용하는 예시
먼저 리플렉션을 사용하기 위해서는 java.lang.reflect.Field 클래스와 setAccessible(true) 메서드를 사용해야 한다. 이 방법을 통해 필드의 접근 제한자를 무시하고 강제로 데이터를 읽어올 수 있다.
▽ 리플렉션을 통해 Prepare 객체의 amount 필드에 접근하는 코드
import java.lang.reflect.Field;
import java.math.BigDecimal;
public BigDecimal getAmountFromPrepare(Prepare prepare) throws Exception {
Field amountField = Prepare.class.getDeclaredField("amount"); // 필드 접근
amountField.setAccessible(true); // 접근 가능하게 설정
return (BigDecimal) amountField.get(prepare); // 필드 값 읽어오기
}
→ 이 코드는 Prepare 객체의 amount 필드에 강제로 접근하여 값을 가져오는 방식이다. setAccessible(true)를 통해 필드가 비공개(private)로 선언되어 있어도 강제로 접근할 수 있게 설정한다. 이렇게 하면 getAmount() 메서드가 없어도 amount 필드의 값을 정상적으로 가져올 수 있다.
▶리플렉션의 장단점
리플렉션은 사용하는 데 주의가 필요하다. 이번에는 읽기 전용 필드에 접근해야 하는 특별한 상황이었기 때문에 리플렉션의 예시를 보긴했지만 자주 사용하기에는 위험한 면이 있다 .
▷ 장점:
→ 접근 불가능한 필드나 메서드에 접근 가능 :
외부 라이브러리나 읽기 전용 데이터에 대한 수정 작업이 가능하다.
→ 동적처리 :
실행 시간에 객체의 구조를 분석하고 처리할 수 있어 유연한 코딩이 가능하다.
▷ 단점:
→ 성능 저하:
리플렉션은 일반적인 메서드 호출보다 성능이 떨어질 수 있다.
→ 코드 안정성 저하:
컴파일 시점에 오류를 잡아내기 어렵고, 런타임에서 문제가 발생할 수 있다.
▶리플렉션과 자바 철학의 충돌
이 기능을 사용하는 것이 자바의 철학에 완전히 부합하지 않는다는 의견이 있다. 자바는 객체지향 원칙과 클래스 간의 캡슐화(Encapsulation)를 중시하는 언어다. 캡슐화는 클래스 내부의 데이터를 보호하고, 외부에서 직접적으로 필드에 접근하는 것을 방지한다. 이는 객체의 일관성을 유지하고, 불필요한 외부 간섭을 막는 중요한 원칙이다.
그러나 리플렉션은 이 원칙을 깨뜨릴 수 있다. 리플렉션을 통해 접근 제한자(private, protected 등)를 무시하고 필드나 메서드에 강제로 접근하면, 원래 의도한 대로 설계된 코드의 보호 장치를 무력화하는 셈이다. 이러한 이유로, 리플렉션 사용은 자바의 철학과 충돌하는 부분이 있으며, 현업에서도 리플렉션의 사용을 지양하는 경우가 많다고한다.
▶ 리플렉션을 대체할 방법?
▷공식 API 확장 요청
아이엠포트 API에 필요한 기능을 요청하는 것이 가장 이상적이다. 공식적인 업데이트로 getAmount()와 같은 메서드가 추가되면 리플렉션을 사용할 필요 없이, 코드의 안정성을 유지할 수 있다.
▷ 데이터 매핑 도구 사용
데이터를 객체에서 직접 가져오는 대신 DTO 패턴을 사용하여 데이터를 매핑하는 방법. 이를 통해 읽기 전용 필드를 간접적으로 처리할 수 있다.
public PaymentPrepareResponseDto mapPrepareToDto(Prepare prepare) {
return new PaymentPrepareResponseDto(
prepare.getMerchantUid(),
prepare.getAmount()
);
}
→ 이 방식으로 실행해봤는데 안됐다 ㅠㅠ.. map이 들어간 예시방법으로 다시 코드를 작성해볼거지만......기대는 안한다
▷ 커스텀 래퍼 클래스 사용
Prepare 객체를 감싸는 래퍼 클래스(Wrapper Class)를 만들어 필드에 간접적으로 접근하는 방식 (X)
public class PrepareWrapper {
private final Prepare prepare;
public PrepareWrapper(Prepare prepare) {
this.prepare = prepare;
}
public BigDecimal getAmount() {
return prepare.getAmount();
}
}
→ Prepare 클래스의 amount 필드는 패키지-private 접근 제어자(package-private, default 접근제어자)를 가지고 있기 때문에, 해당 패키지 외부에서는 이 필드에 접근할 수 없다는 뜻이다. 이미 시도해봤지만 아래의 이유로 포기 ㅠ
→ 패키지-private 접근 제어자:
Prepare 클래스 내의 amount 필드는 private가 아닌 package-private로 설정되어 있어 같은 패키지 내에서만 접근할 수 있다. 하지만 PrepareWrapper 클래스는 com.clean.cleanroom.payment.wrapper 패키지에 있고, Prepare 클래스는 com.siot.IamportRestClient.response 패키지에 있다. 서로 다른 패키지에 있기 때문에 PrepareWrapper에서 Prepare의 amount 필드에 접근할 수 없다.
▷ 빌더 패턴 활용
객체 생성 시 빌더 패턴을 사용하여 필드를 조정하고, 필요한 데이터를 가진 객체를 생성할 수 있다.
public class PrepareBuilder {
private String merchantUid;
private BigDecimal amount;
public PrepareBuilder withMerchantUid(String merchantUid) {
this.merchantUid = merchantUid;
return this;
}
public PrepareBuilder withAmount(BigDecimal amount) {
this.amount = amount;
return this;
}
public Prepare build() {
return new Prepare(this.merchantUid, this.amount);
}
}
▶결론
리플렉션을 사용하여 읽기 전용 필드에 접근하는 방법을 알게 되었다. 그러나 리플렉션은 성능 저하와 코드 안정성 저하의 문제를 유발할 수 있어, 대체 방법도 함께 고려하는 것이 중요하다. 리플렉션은 편리한 도구이긴하지만, 자바의 객체지향 철학과는 충돌할 수 있는 부분이 있다. 필요한 경우에만 신중하게 사용한다는게 잘 이해가 되지않는다. 접근 제한을 무시하고 강제로 데이터를 읽어온다는 말이 그냥 실행만 되면 되는건가..? 데이터의 안전성같은 이유로 이게 맞는건가라는 생각이 들어서 일단 다른방법을 고안해봐야 할 것 같다😟
'개발Article' 카테고리의 다른 글
[TIL]20240914 Gradle과 Maven의 차이점 (0) | 2024.09.15 |
---|---|
[TIL]20240913 내부DB & 그냥DB 그리고 동기화 (1) | 2024.09.14 |
[TIL]20240911 웹과 애플리케이션 (0) | 2024.09.11 |
[TIL]20240910 리젝(Reject) (0) | 2024.09.10 |
[TIL]20240909 스택 트레이스(Stack Trace) (0) | 2024.09.09 |