나의 Spring 전략

공부는 혼자하는 거·2023년 11월 16일
0

Spring Tip

목록 보기
52/52
post-thumbnail

의존관계의 최소화

의존관계가 최소화 되어야 한다는 것은 무슨 의미인가.. 넓게 보면 시스템 아키텍처와 더 나아가서도 생각해 볼 수 있지만, 범위를 한정짓자. 어쨌건 나는 기술자니까, 디테일하게 얘기해보아야 한다. 예시는 SpringBoot + JPA Project로 상정하겠다. 범용적인 토크를 하고자 한다면 관련 예시로는 한계가 있으나, 구체적인 예를 하나 선정해야지만 이해가 더 잘 된다. 나는 선택하자면 후자를 선택하겠다.

코어 의존성은 코어끼리만 의존하도록

예를 들어 다음과 같은 상황이 있다고 치자

보시다시피 UserVerify Entity Class의 인스턴스를 생성하는데 Token이 필요하다. 만약 이렇게 UserVerify 인스턴스를 만드는 코드가 이것 말고도 여지저기 중복되어 있다면 어떻게 할까? 한가지 아이디어는 이 Token을 만드는 코드를 정적 메서드 함수로 따로 빼놓고 재사용하는 것이다.

이렇게 되면, createUserVerify 를 호출하는 여러 군데에서 토큰을 만드는 중복 코드를 엔티티 클래스 한 군데로 몰아넣을 수 있다. 중복을 제거한다는 목적에서 만족스러운 리팩토링이라고 생각이 든다. 그러나, 과연 손실은 없을까?

엔티티가 다른 클래스에 대해 의존하게 된다.

한가지 걱정해야 되는 문제는, 엔티티 클래스가 유틸리티 클래스에 대해 의존을 한다는 것이다.

엔티티 클래스는 일반적으로 서버 로직에서 코어에 위치하게 된다. 이 말인 즉슨 가장 깊숙한 곳에 위치해 있다는 것이며, 모듈을 하나의 모듈로 한정짓지 않고, 여러개의 모듈로 넓혀나갔을 때 다른 모듈들도 같이 이 엔티티를 사용할 가능성이 높다는 뜻이다.

만약에 프로젝트 규모가 커져서 서버를 쪼갠다고 했을 때 떨어져나간 다른 모듈도 이 엔티티를 사용하고 싶다면, TokenUtil이라는 오브젝트도 필요하게 된다. 하지만 떨어져나간 이 모듈은 TokenUtil의 로직이 필요가 없다. 그렇다면 우리는 그 모듈에서 저 엔티티 클래스를 고대로 갖고 오지 못하고 TokenUtil에 관련 코드들을 지워버리거나 같이 가져와야 된다. 코어 의존성에 다른 부가적인 의존성들이 덕지덕지 붙어나가기 시작하면, 모듈은 분리시키기 점점 더 어려워지고 복잡해진다. 정적 유틸리티 클래스는 편하기에 남용될 여지가 있다.

DTO <=> Entity 변환 로직은 DTO에게 담당

같은 맥락에서 얘기를 하자. Spring MVC와 JPA를 사용해서 DATA 지향 API 를 만든다 치면, 보통 Controller <=> Service <=> Repository로 이어지는 흐름을 타게 된다. 이 과정에서 DTO를 주고 받으며, DTO를 Entity로 또는 Entity를 Dto로 변환하게 된다. 꼭 이렇게 해야지만 한다는 법은 없지만, 일반적인 경우에서, 실보다 득이 더 많은 방법이라 판단되기에 대부분 따르는 관습이다. (이렇게 할 시 별도의 Mapper Layer를 만들어 담당하게 하는 경우도 있으나 나는 선호하는 패턴은 아니다.) 그리고 아래와 같이 이렇게 DTO로 변환하는 로직이 Entity Class에 위임되어 있는 경우가 있다.

객체지향적인 관점에서 변화를 하는 주체가 Entity니까 어찌보면 맞는 방향일 수 도 있다. 그러나 Entity Class 가 Dto Class에 대해 의존하게 되는 문제가 발생한다. 따라서 아래와 같이 변경한다.

나는 Entity Layer 에는 필수적인 의존성들만 포함시키고 나머지는 바깥으로 밀어내는 전략을 선호한다. Entity에 존재하는 비즈니스 로직은 자기 스스로에 대한 상태변화만을 의미하게끔 한다.

만약 UpdateDto의 필드가 너무 많을 때 Entity Update

시간 나면 적겠다..

응답형식 설계

일관성 있는 Response를 보장한다면, 클라이언트 측에서도 일관성 있는 로직을 작성하기 쉬워진다. 공통 DTO는 그러한 맥락에서 설계한다. 매번 달라지는 필요한 데이터는 제네릭 타입의 필드로 제한해두면 된다.

이렇게 하면, 클라이언트는 API 호출시 필요한 데이터를 매번 data 라는 이름으로 접근할 수 있다는 사실을 알게 될 거고, 이로 인해 일관성 있는 코드를 작성하는 데 도움을 줄 수 있다. 나는 하나의 API가 성공시 응답코드는 항상 동일한 하나의 결과를 반환받는 걸 선호하는 편이라 이렇게 성공시 응답코드를 2개를 나눈 것에 있어서 좋아하는 설계는 아니다. 하지만 드문 케이스에서 위의 ResultCode 처럼 하나의 API에 대해 성공케이스를 여러개로 나누는 것이 유효한 방법이 될 수 있다.

Controller Layer에서는 반환값을 : SuccessResponse<*> 로 통일하자. 마찬가지로 응답 DTO에 대한 의존성을 줄이려는 목적에 있으며 서비스 레이어에서 반환하는 리턴값이 달라졌을 때, 컨트롤러 레이어까지 미치는 영향력을 최소화하기 위해서이다.

Exception 도 동일하게 응답형식을 일관성 있게 작성하도록 하자.

의존성을 최소화하는 게 꼭 더 나은 방향일까?

이거에 대한 대답은 항상 문맥에 따라 달라진다는 게 답이다. 의존성의 최소화는 유연한 설계, 확장에 강점이 있지만 다른 한편으로는 코드를 다소 이해하기 어렵게 만들고 어떤 경우에는 생산성을 저하시키는 원인이 되기도 한다.

JPA Entity Setter

코틀린으로 JPA Entity를 설계할 때 가장 골치아픈 게 setter 에 대한 문제다. 이러한 문제점은 아래 링크에서 자세히 설명하고 있다.

https://multifrontgarden.tistory.com/272

나 같은 경우는 정말 민감한 필드의 경우, protected set 으로 막아놓고, 나머지는 Setter를 그냥 열어두고 최대한 안 쓰는 방향으로 작업한다.

특정 경우에는 DTO 보다 Map이 더 나은 대안

대부분의 경우 요청 파라미터로 Map을 받는 경우는 없어야 한다. Map을 받게 되면, 해당 API가 요청 값으로 무엇을 받고 있는지 코드를 직접 까보면서 파악해야 되고, DTO 단에서 미리 설정할 수 있는 여러가지 Validation도 서비스 레이어로 내려오게 된다. 하지만 특수한 몇몇 케이스에서는 DTO보다 유용하게 쓰일 수 있는데 가령 다음과 같은 경우다.

내가 제어할 수 없는 외부 API를 가져다 쓰는데 그 API를 신뢰할 수가 없는 경우.

lateinit, Lazy 사용법 팁

간혹 가다, 초기값을 늦게 주고 싶은 변수들을 lateinit 키워드로 할당하는 경우가 있다.

이럴 경우, 정말 할당이 되었는지 체크를 하고 싶을 경우가 있을 수 있을 텐데 다음과 같이 사용하면 된다.

불변값으로 늦게 할당하고 싶을 경우,

Repository Layer에서 비즈니스 로직의 분리

시간 나면 적겠다..

NON-Nullable 타입을 보장받고 싶을 때

시간 나면 적겠다..

Event Publisher 를 고려

시간 나면 적겠다..

Transaction 범위 선정

일반적인 경우의 어플리케이션 서버에서 대부분의 병목지점은 I/O 작업들에서 나타난다. 그리고 I/O 작업의 대표적인 케이스가 데이터베이스 쿼리일 것이다. 고로 DB에서 일어나는 병목현상만 최소화해도 성능개선이 유의미하게 일어나는 뜻. 여기에는 단순 쿼리튜닝만 있는 게 아니라 어플리케이션 레벨에서도 생각보다 많이 고칠 구석이 있다. JPA를 사용하다보면 아마 대부분 아래와 같은 에러를 맞닥뜨렸을 거라고 생각한다.

Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection

DB Connection Pool 이 고갈되서, transaction을 시작할 수 없다는 의미이다. 무식한 방법은 Connection Pool을 단순 늘릴 수 있겠지만, 좀 더 디테일하게 들어가서 트랜잭션의 범위에 대해서도 고민해볼 필요가 있다. 트랜잭션이 시작되는 메소드를 가만히 뜯어보면, 그 안에서 트랜잭션이 필요없거나, 하나의 트랜잭션으로 묶일 필요가 없는 로직들이 들어가 있는 경우가 있다. 그런 부분들은 별도로 분리해서 트랜잭션 범위를 최소화 할 수 있는 만큼 적용해준다. 나는 이럴 경우 별도의 함수로 분리하고, @TransactionalEventListener 로 다루는 걸 선호하는 편이다.

비동기 함수 호출 시 주의사항

시간 나면 적겠다..

결론

소프트웨어 디자인에서 정해진 답은 없다. 각자의 맥락 속에서 더 유효한 방법만이 존재할 뿐이다. 그래도 내가 얘기하고자 하는 것은 일반적인 문맥에서 내가 지키고자 하는 원칙 같은 것이다.

profile
시간대비효율

0개의 댓글