개발 공부/스프링

백엔드 계층 구조 정리 1

baby-t 2026. 3. 11. 21:34

1. Entity (도메인 모델)

  • 정의: DB의 테이블과 1:1로 매핑되는 자바 객체.
  • 기본 생성자 필수 (@NoArgsConstructor): * JPA(하이버네이트)가 DB에서 데이터를 가져와 자바 객체로 조립할 때 리플렉션(Reflection) 기술을 사용함. 리플렉션으로 객체를 껍데기부터 생성해야 하므로 파라미터가 없는 기본 생성자가 무조건 필요함.
    • (💡 실무 Tip: 무분별한 객체 생성을 막기 위해 access = AccessLevel.PROTECTED로 막아두는 것이 국룰)
  • 관계 매핑: 객체 간의 연관 관계(1:N, N:1 등)를 어노테이션(@ManyToOne 등)으로 설정하여 객체 지향적으로 DB를 다룰 수 있게 함.
  • 지연 로딩 (FetchType.LAZY):
    • 연관된 엔티티를 미리 다 가져오지 않고, 실제 해당 데이터가 쓰이는 시점에 DB에 쿼리를 날려 가져오는 기술.
    • 즉시 로딩(EAGER)을 쓰면 쓸데없는 연관 데이터까지 다 가져오느라 쿼리가 폭발하므로, 실무에서는 모든 매핑에 무조건 LAZY 속성을 걸어두는 것이 원칙이다.

2. Repository (데이터 접근 계층)

  • 정의: 실제로 JPA를 통해 DB에 접근하고 데이터를 조작(CRUD)하는 역할을 하는 계층.

[JPA와 영속성 컨텍스트의 핵심 원리]

  • ORM (Object-Relational Mapping): 객체지향 프로그래밍의 '객체'와 관계형 데이터베이스의 '테이블'을 자동으로 연결(매핑)해 주는 기술.
  • JPA & 하이버네이트: * JPA: 자바 진영의 ORM 표준 명세서(인터페이스/규칙).
    • Hibernate(하이버네이트): 이 JPA 규칙을 받아서 실제로 기능을 수행하는 구현체(엔진).
  • 영속성 컨텍스트 (Persistence Context) & EntityManager:
    • 정의: 애플리케이션과 DB 사이에 존재하는 '가상의 논리적 데이터 저장 공간'.
    • EntityManager: 이 영속성 컨텍스트에 데이터를 넣고 빼고 삭제하는 실질적인 관리자.
    • 🔥 영속성 컨텍스트의 4대 마법:
      1. 1차 캐시: 한 번 조회한 데이터는 메모리에 들고 있어서, 똑같은 걸 또 찾으면 DB에 안 가고 캐시에서 바로 줌.
      2. 지연 쓰기: 삽입/수정 쿼리를 바로 쏘지 않고 모아뒀다가, 트랜잭션이 끝날 때(commit) 한 번에 쏴서 DB 부하를 줄임.
      3. 변경 감지 (Dirty Checking): 객체를 DB에서 꺼내와서 데이터 값만 수정하면, 나중에 save()를 호출하지 않아도 트랜잭션 종료 시 알아서 UPDATE 쿼리가 날아감.
      4. 지연 로딩: 앞서 말한 Entity의 LAZY 설정을 지원하는 공간.

[Spring Data JPA의 구조]

  • 우리가 작성하는 public interface UserRepository extends JpaRepository<User, Long>의 원리.
  • JPA를 더 쓰기 편하게 만든 스프링의 모듈.
  • JpaRepository는 내부적으로 PagingAndSortingRepository ➡ CrudRepository를 차례대로 상속받고 있음. 덕분에 개발자가 별도의 구현 코드 없이도 save(), findById(), 페이징 처리 등 강력한 기본 메서드들을 공짜로 사용할 수 있음.

3. Service (비즈니스 로직 계층)

  • 정의: 애플리케이션의 핵심 목적(비즈니스 로직)을 수행하고, 컨트롤러와 레포지토리 사이에서 데이터를 가공 및 연결하는 중심 계층. (트랜잭션이 시작되고 끝나는 경계)
  • 스프링 빈(Bean)과 의존성 주입(DI):
    • IoC 컨테이너 (스프링 컨테이너): 스프링은 실행될 때 필요한 객체들(Service, Repository 등)을 딱 하나씩만 생성해서(싱글톤) 메모리에 띄워두고 관리함. 이 객체들을 **빈(Bean)**이라고 부름.
    • DI (Dependency Injection): 서비스가 레포지토리를 사용해야 할 때 직접 new로 생성하지 않고, 스프링이 만들어둔 빈을 외부에서 주입받아 사용함.
    • *(💡 실무 Tip: 주입을 받을 때는 필드 주입(@Autowired) 대신, 롬복의 **@RequiredArgsConstructor를 활용한 '생성자 주입'*을 사용하는 것이 국룰. 순환 참조를 방지하고 객체의 불변성을 보장할 수 있기 때문임.)
  • 트랜잭션 관리 (@Transactional):
    • 데이터베이스의 상태를 변경하는 작업 단위(예: 출금 ➡ 입금)를 하나로 묶어줌. 중간에 에러가 나면 모든 작업을 원상 복구(Rollback)시켜 데이터의 정합성을 보장하는 핵심 어노테이션.
    • (💡 실무 Tip: 클래스 상단에 @Transactional(readOnly = true)를 걸어 조회 성능(변경 감지 비활성화 등)을 높이고, 데이터 조작(C, U, D)이 일어나는 메서드에만 @Transactional을 덮어씌우는 패턴을 주로 사용함.)

4. Test Code (테스트 코드)

  • 정의: 내가 작성한 코드가 의도한 대로 정확히 동작하는지 검증하기 위해 작성하는 코드. (주로 src/test 디렉토리에 위치하며 JUnit5를 사용)
  • 스프링 컨테이너의 이해: 테스트를 진행할 때, 실제 서버를 띄울 때처럼 거대한 바구니(스프링 컨테이너)에 모든 빈(객체)을 담아두고 테스트할 것인지, 아니면 가짜 객체를 쓸 것인지에 따라 두 가지로 나뉨.

[테스트의 두 가지 종류]

  1. 통합 테스트 (Integration Test)
    • 사용: 클래스 상단에 @SpringBootTest 어노테이션을 붙임.
    • 특징: 실제 프로젝트를 실행하는 것과 똑같이 스프링 컨테이너를 통째로 띄움. 빈으로 등록된 진짜 Service, Repository, DB 연결까지 모두 사용하여 테스트함.
    • 장단점: 실제 환경과 동일하므로 신뢰도가 매우 높지만, 컨테이너를 띄우고 DB를 연결하느라 실행 속도가 느림. (@Autowired를 사용해 객체를 가져옴)
  2. 단위 테스트 (Unit Test)
    • 사용: 스프링 컨테이너를 띄우지 않고, Mockito 같은 라이브러리를 활용함.
    • 특징: 특정 클래스(예: Service)의 로직만 집중적으로 테스트하기 위해, 연관된 Repository 등은 **가짜 객체(Mock)**로 흉내 내어 주입함. (@Mock, @InjectMocks 활용)
    • 장단점: 스프링을 띄우지 않기 때문에 실행 속도가 0.1초 단위로 매우 빠름. 비즈니스 로직 자체의 예외 처리나 계산 로직을 촘촘하게 검증할 때 유리함.

[주요 어노테이션]

  • @Test: 이 메서드가 테스트 메서드임을 선언.
  • @BeforeEach / @AfterEach: 각 테스트가 실행되기 직전/직후에 실행할 공통 로직 (예: 테스트용 DB 데이터 초기화).
  • @DisplayName("~를 테스트한다"): 테스트 결과창에 보여줄 예쁜 이름을 지정.

5. DTO (Data Transfer Object)

  • 정의: 계층 간(주로 Controller ↔ Service ↔ Client) 데이터 교환을 위해 사용하는 순수한 데이터 객체. (비즈니스 로직은 포함하지 않음)
  • 역직렬화(Deserialization)와 직렬화(Serialization): 클라이언트와 서버는 서로 다른 언어를 쓰기 때문에 JSON 형식을 통해 데이터를 주고받음.
    • Request DTO (역직렬화): 클라이언트가 보낸 JSON 데이터 ➡ 자바 객체.
      • Jackson 라이브러리가 이 변환을 담당함. Jackson은 리플렉션을 통해 빈 객체를 먼저 생성한 뒤 필드에 값을 꽂아 넣기 때문에 기본 생성자(@NoArgsConstructor)가 무조건 필수임.
    • Response DTO (직렬화): 서버의 자바 객체 ➡ 클라이언트에게 JSON으로 반환.
      • Jackson이 완성된 자바 객체의 값을 '읽어서' JSON으로 변환하므로 기본 생성자는 필요 없고, 값을 읽기 위한 @Getter가 무조건 필수임.
  • 데이터 검증(Validation)의 최전선:
    • 클라이언트의 잘못된 입력이 DB(Entity)까지 도달하기 전에 차단하는 역할.
    • DTO 필드에 @NotNull, @NotBlank, @Min(1) 등의 제약 조건을 달아두고, Controller에서 @Valid 어노테이션과 함께 사용하여 입력값이 들어오자마자 예외를 발생시킴.