실무에 도입해볼만한 Java Spring 설계 규칙들

이신영·2025년 6월 23일
0
post-thumbnail

왜 이런 규칙이 필요할까?

웹 개발을 하다 보면 구조가 엉키고, 어느새 코드가 유지보수하기 어려워지는 경험을 하게 된다.
다양한 프로젝트를 겪다보니 그 상황에 맞는 깨끗한 설계의 필요성을 절감하면서 몇 가지 규칙들을 스스로 세우게 됐다.

이 글에서는 내가 경험적으로 적용해왔고, 실무에 바로 도입해볼 수 있는 Java Spring 기반 웹 개발 설계 규칙들을 정리해본다.


1. Controller는 입출력만 담당한다

Controller는 말 그대로 HTTP 요청을 받아 처리하고, 응답을 내려주는 역할만 담당하는 게 좋다.
입력값을 DTO로 받고, 결과를 ResponseDTO로 변환해 내려주는 구조가 명확하다.

이렇게 하면 테스트도 수월하고, 변경에 유연한 구조를 만들 수 있다.

그런데 이렇게 되면 Service 계층이 너무 복잡해지는 건 아닌가? 라는 의문이 들 수도 있다.
실제로 Service가 무거워지는 경우가 많다. 그럴 땐 다음처럼 분리하는 걸 추천한다.

  • 유효성 검사: Validator 혹은 Checker 클래스에서 수행
  • 포맷 변환, 계산 로직: Util 혹은 Helper로 분리
  • 외부 연동 처리: Client 또는 Adapter 클래스 따로 분리

핵심 비즈니스 로직만 Service에 남기고 나머지를 분리하면, 오히려 Service가 더 깔끔해진다.


2. Service / ServiceImpl 구분

Service는 인터페이스와 구현 클래스를 나누는 걸 기본 구조로 가져간다.
테스트 코드 작성이나 확장성 측면에서 유리하다.
특히 프로젝트 규모가 커지거나, 여러 구현체가 필요한 상황에서 효과를 본다.

사실 좀 관례인 것 같기도하다 😅 구현체를 좀 자주 바꾸는 경우는 확실히 유용하다.


3. RequestDTO와 ResponseDTO는 구분한다

입력과 출력은 책임이 다르다.
사용자가 입력하는 필드와 클라이언트에게 보여줘야 할 필드는 완전히 다를 수 있다.

예를 들어, 사용자의 패스워드나 내부 상태 값은 응답에 포함되면 안 된다.
별도의 DTO로 구분하면 이런 실수를 줄일 수 있다.

구현이 어렵다면 초기엔 DTO를 통합하고 그 후 분리를 하는걸 추천한다.


4. Entity와 DTO는 철저히 분리

Entity 객체를 그대로 응답으로 내려주거나, 사용자 입력을 그대로 매핑하는 방식은 지양한다.
JPA의 lazy-loading, dirty checking 등에 의도치 않은 영향이 갈 수 있다.
DTO ↔ Entity 간 변환은 Mapper 혹은 Converter 클래스를 별도로 만들어 처리하는 것이 좋다.


5. Lombok @Data 대신 필요한 어노테이션만 사용

@Data는 너무 많은 걸 한 번에 생성해버린다.
toString, equals, hashCode 등이 자동 생성되기 때문에 불필요한 연산이나 순환 참조 문제가 발생할 수 있다.
@Getter, @Setter, @Builder 등을 필요한 만큼 명시적으로 사용하는 게 좋다.

그 외에도 어노테이션은 개발에 편의성을 제공하지만 그게 어떤 기능인지 정확하게 알고있어야한다. 협업시에 어노테이션으로 로직을 숨긴다면 모두가 이걸 알고있어야함 (문서화 추천)


6. 생성자 주입 사용

불변성을 유지할 수 있고, 테스트 코드 작성이 더 수월하다.
@RequiredArgsConstructorfinal 키워드를 활용해 필드 주입을 없애는 걸 기본 원칙으로 삼는다.


7. 예외 처리 통합

Controller마다 예외를 처리하는 대신, 전역 예외 처리기를 만들어 일관성 있게 관리한다.
@RestControllerAdvice를 사용해 예외 타입별로 명확하게 대응하고,
클라이언트에는 통일된 형태의 에러 응답을 내려주도록 구성한다.


8. 패키지 구조는 도메인 중심으로 나눈다 (규모가 크다면)

패키지를 controller/service/repository로 나누는 전통적인 방식도 나쁘진 않다.
하지만 일정 규모 이상의 프로젝트에서는 도메인 단위로 나누는 것이 더 명확하고 응집도 높은 구조를 만든다.


user/
├── controller
├── service
├── domain
├── dto
└── repository

이런 구조는 관련된 코드가 한곳에 모여 있어 파악이 쉽고, 모듈화를 고려할 때 유리하다.

물론 단점도 있다.
공통 유틸이나 여러 도메인이 사용하는 기능의 위치가 애매해지는 경우가 있다.
이럴 땐 common, shared 같은 패키지를 별도로 두어 처리하는 방식으로 보완한다.


9. 비즈니스 로직 외 기능은 Service에서 분리

전화번호 포맷팅, 이메일 유효성 검사처럼 도메인과 크게 관련 없는 로직은
Service에 넣지 말고 Validator, Util 등의 별도 클래스로 분리한다.

이렇게 하면 Service는 도메인 중심의 로직만을 담당하게 돼, 가독성과 테스트성이 올라간다.


10. REST 응답 포맷 통일

성공/실패 여부, 데이터, 에러 메시지를 일관된 포맷으로 응답하면 클라이언트와의 계약이 명확해진다.

{
  "success": true,
  "data": {...},
  "error": null
}

이 구조를 기본 응답 형태로 정의하고 ResponseWrapper 같은 클래스를 만들어 관리하면 좋다.


위 정도만 대입해도 충분히 프로젝트가 한결 유지보수하기 쉬워진다 아래부터는 좀 더 깊게 고민해볼 것들이다.

더 깊게 들어가볼 설계 요소들

도메인 모델에서 비즈니스 로직 수행

서비스에서 모든 계산을 담당하기보다는, 도메인 객체가 스스로 책임지도록 설계하는 게 바람직하다.

public class Order {
    private List<OrderItem> items;

    public Money calculateTotalPrice() {
        return items.stream()
            .map(OrderItem::getPrice)
            .reduce(Money.ZERO, Money::add);
    }
}

Order가 스스로 가격을 계산하는 구조가 더 자연스럽고 테스트도 쉬워진다.


Enum과 Value Object 활용

의미 있는 값은 단순 String이나 int로 표현하지 말고, Enum이나 VO로 감싸는 게 명확하고 안전하다.

public enum OrderStatus {
    PENDING, PAID, CANCELLED
}
public class Email {
    private final String value;

    public Email(String value) {
        if (!value.matches(VALID_PATTERN)) throw new IllegalArgumentException();
        this.value = value;
    }
}

이유 ?

Enum 특성상 컴파일 시점에 잡혀서 잘못된 값이 들어갈 수 없기 때문 → 타입안정성


설정 클래스는 역할별로 분리

모든 설정을 AppConfig에 몰아넣지 말고, 역할별로 나눠서 관리하면 훨씬 명확하다.

@Configuration
public class WebConfig {}

@Configuration
public class SwaggerConfig {}

@Configuration
public class RedisConfig {}

특히나 api를 추가하다보면 관리할게 많아지니까 따로 분리해야하고 또, 스프링 레거시 프로젝트라면 더더욱 구조화를 잘 시켜둬야한다.


테스트 구조도 계층별로 구분

  • 단위 테스트는 service, domain 중심으로 작성
  • 통합 테스트는 @SpringBootTest를 활용
  • Controller 테스트는 @WebMvcTest, 외부 API는 @RestClientTest

테스트의 목적이 명확하면 코드도 안정적이고, 리팩토링도 수월해진다.


마치며

위에서 소개한 규칙들은 정답이라기보다는, 내가 프로젝트에서 겪은 경험을 토대로 정리한 기준들이다.

전반적으로 느껴지는 키워드는 책임에 대한 분리와 명확한 표현 이라고 생각한다.

처음부터 완벽한 구조를 만드는 건 어렵지만,
작은 습관부터 정리해나가다 보면 유지보수하기 좋은 구조를 자연스럽게 갖추게 된다.

협업을 위한 코드, 변화에 유연한 구조를 고민하고 있다면 이 글이 조금이나마 도움이 되었으면 한다 👍

profile
후회하지 않는 사람이 되자 🔥

0개의 댓글