[프로젝트] 냉철하게 평가하는 회원API 프로젝트
얼마전에 Rest Api 를 개발한 적이 있다. 열심히 만든 과제이지만 환경설정, 아키텍쳐 구상에서 시간을 많이 할애해서 막상 냉철하게 내가 짠 코드를 살펴본 시간이 부족했던 것 같다.
특히, 나의 마인드를 고칠 계기가 된 것 같다. 나는 이런사람이었던 것이다.
내가 짠 코드에는 관대하고 다른 사람이 짠 코드에는 냉정하다. 😑
이번 경험으로 나는 내 코드를 다시 한번 냉철하게 생각해보려고 한다.
불변 클래스
이펙티브 자바에서 배웠듯이, 불변 객체는 가변 클래스보다 설계, 구현, 사용하기 쉬우며 오류가 생길 여지도 적고 훨씬 안전하다. 멀티 스레드 환경에서 객체가 변경될 가능성이 1도 없기 때문이다.
Validation Utils 과 같이, 애플리케이션이 한번 구동되면 변하지 않는 성격을 가진 객체를 불변 클래스로 만들고, 클래스 안에 있는 heavy한 객체 Pattern
인스턴스를 캐싱해서 사용한다.
여기서 나의 실수는 저렇게 생각해놓고 만든 클래스가 불변클래스가 아니라는 점이다 ㅋㅋ (어이 없다 생각만해도 😐)
불변 클래스를 만드는 방법은 다음과 같다.
- 객체의 상태를 변경하는 메서드(setter)를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다. (
final
클래스로 만든다.) - 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. (방어적 복사를 수행하라)
static
필드의 초기화
public class RedisUtils {
private static RedisTemplate redisTemplate;
public RedisUtils(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public static Object getValueForHash(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
}
클라이언트 측에서 RedisUtils.getValueForHash(key, hashKey)
를 호출해서 객체를 생성하지 않고도 레디스 함수를 간단히 사용하게 하고 싶었다.
하지만 단단히 잘못된 코드다. getValueForHash()
메서드는 static
이다. 그렇다면, RedisUtils
객체를 초기화해서 redisTempalte을 주입받지 않아도 저 함수를 실행시킬 수 있다는 뜻이다! 😫
@Test
public void 레디스_실패_테스트() {
RedisUtils.getValueForHash("Test", "Test");
}
// java.lang.NullPointerException 발생!
이걸 고치는 방법은 2가지가 있을 것 같다.
static
을 빼서, 클라이언트에서 항상redisTemplate
을 주입한 instance 를 생성해서 사용한다.- 싱글톤으로 만들어서 주입받아서 사용한다.
- 그래도
static
을 포기하지 못한다면, RedisUtils 클래스가 생성되고 나서,redisTemplate
를 직접 주입시켜준다.@Component
로 싱글턴 객체를 만들어준다.@PostConstruct
로 클래스가 로드된 후, 객체를 static 필드에 주입시켜준다.
- 아니면 생성자에
@Autowired
를 붙이는 방법도 있다. 생성자를 private으로 선언하므로써, 클라이언트가 이 객체를 생성할 수 없게 만든다.- Example
@Autowired private RedisUtils(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; }
- Example
클래스 로드 후 객체를 주입시키는 방법
@Component
public class RedisUtils {
private static RedisTemplate redisTemplate;
@Resource(name = "redisTemplate")
private RedisTemplate redisTemplateInstance;
@PostConstruct
private void init() { redisTemplate = redisTemplateInstance; }
public static Object getValueForHash(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
}
Exception 의 종류
사실, 회사에서는 공통된 Exception 클래스가 있고 비즈니스 에러가 발생될 경우 그 클래스를 무조건 써서 Exception 에 대한 지식이 많이 없었다.
그나마 NPE, IndexOutOfRangeException 등과 같은 에러들은 운영에서 확인을 자주 할 수 있어서 익숙했지만, 내가 만든 API에서 어떤 오류를 던질지에 대한 정리가 잘 안되었던 것 같다.
Effective-Java 아이템 68까지 하고 자체 여름방학을 가진것이 화근이었던 것 같다. 아이템 69부터 예외인데!! 😥
다시 공부를 시작해야겠다. 일단 지금 생각날 때 아이템72:표준 예외를 사용하라 를 읽는다.
패키지 구성
사실 패키지 구성은 무난하게 기본을 따랐다고 생각했다. 하지만 다시 돌이켜보니, 내가 아닌 다른사람이 봤을 때 이 클래스는 왜 이 패키지에 있지? 의아했을 것 같다.
먼저, 개선포인트를 잡기 전에 패키지 구조에 대한 글을 Remind 한다. [개발방법론] 계층별, 기능별 패키지 구성하기
├─main
│ ├─java
│ │ └─com
│ │ └─****
│ │ └─member
│ │ ├─config
│ │ ├─controller
│ │ ├─domain
│ │ │ └─enums
│ │ ├─framework
│ │ ├─mapper
│ │ ├─service
│ │ └─utils
나는 (회사 짬밥이 있으니) 계층별로 패키지는 잘 나누었다고 생각한다.
하지만 framework 패키지 밑에 있는 클래스들이 다른 사람 입장에서는 이게 왜 여기 있지? 라는 생각이 들 수 있을 것 같았다.
2021-09-21 오전 02:05 1,106 ExceptionAdvice.java
2021-09-21 오전 02:05 393 PageDto.java
2021-09-21 오전 02:05 615 ResponseData.java
PageDto
는 각 API에서 페이징된 결과를 담아서 리턴해주기 위한 공통 도메인 기능으로 생성했다.
ResponseData
는 각 API에서 리턴되는 결과를 응답상태, 응답메시지, 데이터 를 묶어서 리턴해주기 위해 생성한 공통 도메인이다.
필자는 위와 같은 이유로 framework
패키지에 클래스를 위치했다. 하지만 네이밍도 애매모호하고 API 패키지 안에 있으니 Dto가 왜 또 여기에 있지? 싶었던 것 같다.
그래서 아래 방식으로 개선하고자 한다.
ResponseData
->ResponseDto
로 네이밍 변경PageDto
,ResponseDto
를 묶은 패키지를 새로 생성common.dto
- 해당 패키지를 떼서 모듈로 만들어 종속되게 하기
이렇게 하면 좀더 깔끔해 지지 않을까?
Controller 와 Service의 책임 분리
나는 여태까지 “Controller 에서는 요청받은 파라미터에 대한 Validation을 수행하고 Service 에서는 비즈니스로직을 수행한다.” 의 패턴으로 코드를 짜왔다.
즉, Service 레이어 까지 가기전에 Controller에서 거를 수 있는 데이터는 걸러내도록 만들었다.
과연 나는 정말 그렇게 만들었을까?
이번에도 먼저 MVC 패턴에 대해 다시 한번 공부하고 가보자. https://umbum.dev/1066 포스팅에서 MVC 패턴을 다시 한번 상기시킨다.
이 글에서 감명깊었던 것은 이 문장이다.
요청이 UI를 통해 들어온거라면 Controller를 거치겠지만, 다른 경로로 들어왔다면? 내부 API 호출이라면? Controller를 거치지 않거나 다른 Layer를 통해서 Service Layer에 접근한다면?
이 관점에서 봤을 때, Controller 에서 파라미터의 Validation을 잡는것은 옳지 않다.
UI를 웹이 아니라 앱으로 변경했을 때, Controller 가 아예 제거되거나, 변경이 발생해야 하는데 Controller 없이도 API 기술 내용이 제대로 동작해야하는 것이다.
그렇다면 Controller은 뭐하는 것이냐! 라고 생각해보면, Conroller 는 Presentation Layer에 데이터를 제공하기 위한 매개체라고 생각하면 편할 것 같다.
바깥으로 데이터를 전송하기 위해 존재하는 것이다. (UI specific하다) 그렇다면 Service에서 받은 데이터를 가공하는 것은 Controller 에서 해도 될까?
Response 에 message 를 담고 싶은데, 이건 Controller에서 해도 되는 건가? 이 고민에 대해서는 Yes라고 정했다. 왜냐하면 ReponseDto 는 Http 응답코드와, API 개발자가 넣고 싶은 message, 그리고 return 되는 Data를 가지고 있다. 이것은 restAPI를 위한 데이터이므로 가공은 Controller에서 하는 것이 맞다고 생각한다.
추후, 다른 엔드포인트로의 (Controller가 아닌) 제공이 필요할 경우에는, 그 게이트웨이를 따로 만드는게 맞다고 생각이 든다.
단순 요청/응답/전달을 위한 DTO와 비즈니스로직에서 사용하는 actor 그 자체인 Model 클래스는 다르게 바라보는 것이 좋다. e.g., Model: User / DTO : UserReqeust & UserResponse
여기서 도메인의 분리를 다시 한번 생각해봤다. 나는 Table 기준 Domain을 리턴하는 데이터로 바로 쓰고 있었다. 이 부분은 도메인 경계에 대한 뚜렷한 정의 없이 개발했기 때문에, 그리고 나의 게으름이 만든 결과라고 생각한다.
인증/인가 에 대한 지식
인증이라는 도메인은 처음 접해보는 내용이었기 때문에 처음에 이걸 어떻게 restFul 하게 만들 수 있을까? 고민을 많이 했던 것 같다.
결국 jwt를 이용한 인증/인가 방법이 현재 제일 트렌디한 방법이라고 판단이 된다. (feat 동료 개발자 의견 추가 😅)
[ ] 인증/인가에 대해 깊게 공부하자
Conclusion
사람은 내 집에서 나가봐야 정신을 차린다는 말이 맞는 것 같다. 편안한 Condition에 안주하고 있으면 뭐가 잘못되어있는지도 모른다. 앞으로 열심히 공부해야한다는 마음가짐이 생긴 계기였다.
댓글남기기