[Spring/입문] 3 Layer Architecture, IoC/DI, Spring IoC Container, JPA

JM·어제

1. 3 Layer Architecture

기존 메모장 프로젝트에서는 하나의 Controller에서 API 요청부터 DB 작업까지 모두 처리하고 있었다.

기능이 많아질수록 한 클래스에 코드가 몰리게 되고, 수정이나 유지보수가 어려워진다.

이를 해결하기 위해 역할을 Controller → Service → Repository로 분리한다.

Controller

클라이언트의 요청을 받는 역할

  • HTTP 요청 처리
  • Request 데이터 전달
  • Service 호출
  • 처리 결과를 Client에게 응답

Service

비즈니스 로직을 담당

  • 사용자의 요구사항 처리
  • 비즈니스 로직 수행
  • DB 작업이 필요하면 Repository에 요청

Repository

DB와 직접적으로 통신하는 역할

  • DB 연결 및 관리
  • CRUD 작업
  • SQL 실행

전체 흐름

Client
  ↓
Controller
  ↓
Service
  ↓
Repository
  ↓
Database

각 계층의 역할을 분리하면 코드의 가독성, 유지보수성, 확장성을 높일 수 있다.


2. IoC와 DI

의존성

한 객체가 다른 객체를 필요로 하는 관계를 의미한다.

예를 들어 Consumer가 Chicken을 직접 생성해서 사용한다면 두 객체가 강하게 결합되어 있다.

public class Consumer {
    void eat() {
        Chicken chicken = new Chicken();
        chicken.eat();
    }
}

이 상태에서는 Chicken을 Pizza로 변경하려면 Consumer의 코드까지 수정 필요

약한 결합

인터페이스를 사용하면 객체 간 결합도를 낮출 수 있다.

interface Food {
    void eat();
}

Consumer는 Chicken이나 Pizza 자체가 아니라 Food에 의존한다.

Consumer → Food
             ↑
       Chicken / Pizza

따라서 실제 구현체가 변경되어도 Consumer의 코드를 크게 수정하지 않아도 된다.


DI (Dependency Injection)

필요한 객체를 외부에서 주입받는 것

객체가 필요한 의존성을 직접 생성하지 않고 외부에서 전달받는다.

대표적인 방법은 생성자 주입이다.

public class Consumer {

    private final Food food;

    public Consumer(Food food) {
        this.food = food;
    }
}

생성자 주입의 장점

  • 객체 간 결합도 감소
  • 의존성 관리 용이
  • 테스트 용이
  • 객체의 불변성 확보

Spring에서는 일반적으로 생성자 주입을 권장한다.


IoC (Inversion of Control)

제어의 역전

객체의 생성과 의존성 관리에 대한 제어권을 개발자가 직접 갖는 것이 아니라 Spring이 관리하는 구조이다.

기존에는

Controller → Service 생성
Service → Repository 생성

과 같이 객체를 직접 생성했다.

DI를 적용하면

Repository → Service → Controller

형태로 필요한 객체를 외부에서 주입받는다.

즉, 객체의 생성과 의존성 관리에 대한 제어가 역전된다.


3. IoC Container와 Bean

Bean

Spring이 생성하고 관리하는 객체

Spring은 애플리케이션 실행 시 필요한 객체를 생성하고 IoC Container에서 관리한다.

IoC Container

Spring Bean을 생성하고 관리하는 컨테이너

Spring IoC Container
 ├─ Controller Bean
 ├─ Service Bean
 └─ Repository Bean

@Component

클래스를 Spring Bean으로 등록할 때 사용한다.

@Component
public class MemoService {
}

@ComponentScan

@Component가 붙은 클래스를 찾아 Bean으로 등록한다.

@SpringBootApplication에 기본적으로 설정되어 있다.


4. Spring의 3 Layer Annotation

Spring에서는 각 계층의 역할을 나타내는 애너테이션을 제공한다.

Annotation역할
@ControllerController 역할
@RestControllerREST API Controller
@ServiceService 역할
@RepositoryRepository 역할

이 애너테이션들은 모두 @Component를 포함하고 있기 때문에 Spring Bean으로 등록된다.


5. JPA란?

ORM

Object-Relational Mapping

객체와 관계형 데이터베이스의 데이터를 매핑하는 기술이다.

기존에는 DB 작업을 위해 직접 SQL을 작성하고 JDBC를 사용해야 했다.

Java 객체
   ↓
SQL 작성
   ↓
JDBC 실행
   ↓
Database

데이터 구조가 변경되면 SQL과 객체 변환 코드도 함께 수정해야 하는 문제가 있다.

JPA

Java Persistence API

Java ORM 기술에 대한 표준 명세

JPA를 사용하면 객체를 중심으로 DB 데이터를 다룰 수 있다.

Java 객체
   ↓
JPA
   ↓
Database

개발자가 직접 SQL을 작성하는 작업을 줄이고 객체 중심으로 DB를 관리할 수 있다.

Hibernate

JPA의 구현체 중 하나

Spring Boot에서는 기본적으로 Hibernate를 사용한다.

JPA = 표준 명세
Hibernate = JPA 구현체


6. Entity

JPA가 관리하는 객체

Entity 클래스는 DB 테이블과 매핑된다.

@Entity
@Table(name = "memo")
public class Memo {

    @Id
    private Long id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false, length = 500)
    private String contents;
}

주요 Annotation

Annotation역할
@EntityJPA가 관리하는 Entity로 지정
@Table매핑할 테이블 지정
@Column필드와 컬럼 매핑
@Id기본 키 지정
@GeneratedValue기본 키 자동 생성

@GeneratedValue

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

DB의 AUTO_INCREMENT와 같이 기본 키 생성을 DB에 위임할 수 있다.


7. 영속성 컨텍스트

Entity 객체를 효율적으로 관리하기 위한 공간

JPA는 Entity를 바로 DB에 저장하는 것이 아니라 영속성 컨텍스트에서 관리한다.

EntityManager

영속성 컨텍스트에 접근하여 Entity를 관리하는 객체

EntityManager
      ↓
영속성 컨텍스트
      ↓
Entity

Entity의 저장, 조회, 수정, 삭제 등을 관리한다.


8. 영속성 컨텍스트의 주요 기능

1차 캐시

영속성 컨텍스트 내부에는 Entity를 저장하는 캐시가 존재한다.

1차 캐시

Key   →   Value
@Id       Entity

em.find()를 호출했을 때 먼저 1차 캐시를 확인한다.

em.find()
   ↓
1차 캐시 확인
   ↓
있음 → 캐시에서 반환
   ↓
없음 → DB 조회 → 1차 캐시 저장 → 반환

1차 캐시의 장점

  • DB 조회 횟수 감소
  • 같은 Entity에 대한 객체 동일성 보장

쓰기 지연

JPA는 INSERT, UPDATE, DELETE SQL을 바로 DB에 보내지 않고 쓰기 지연 저장소에 모아둘 수 있다.

트랜잭션이 커밋되거나 flush()가 호출되면 SQL을 DB에 반영한다.

Entity 변경
   ↓
쓰기 지연 저장소
   ↓
flush()
   ↓
DB

flush()

영속성 컨텍스트의 변경 내용을 DB에 반영

쓰기 지연 저장소에 있는 SQL을 DB에 전달한다.

단, INSERT, UPDATE, DELETE와 같은 변경 작업을 DB에 반영하려면 트랜잭션이 필요하다.


변경 감지 (Dirty Checking)

JPA에서는 별도의 update() 메서드를 호출하지 않아도 Entity의 변경 내용을 감지하여 UPDATE SQL을 생성한다.

Entity 조회
   ↓
최초 상태 저장
   ↓
Entity 값 변경
   ↓
트랜잭션 commit
   ↓
flush()
   ↓
최초 상태와 현재 상태 비교
   ↓
변경된 내용이 있다면 UPDATE SQL 생성

따라서 다음과 같이 Entity의 값만 변경해도 된다.

Memo memo = em.find(Memo.class, id);

memo.setUsername("Update");
memo.setContents("변경된 내용");

이후 트랜잭션이 커밋되면 변경 사항이 DB에 반영된다.


9. Entity의 상태

JPA에서 Entity는 영속성 컨텍스트와의 관계에 따라 상태가 달라진다.

비영속 (Transient)

Memo memo = new Memo();

아직 영속성 컨텍스트에서 관리하지 않는 상태

영속 (Managed)

em.persist(memo);

영속성 컨텍스트가 Entity를 관리하는 상태

준영속 (Detached)

em.detach(memo);

영속 상태였던 Entity를 영속성 컨텍스트에서 분리한 상태

준영속 상태에서는 JPA의 변경 감지 등의 기능을 사용할 수 없다.

삭제 (Removed)

em.remove(memo);

Entity를 삭제 대상으로 지정한 상태

상태 정리

비영속
  ↓ persist()
영속
  ↓ detach()
준영속

영속
  ↓ remove()
삭제

10. 영속성 컨텍스트 관리 메서드

persist()

비영속 Entity를 영속 상태로 만든다.

em.persist(memo);

find()

Entity를 조회한다.

em.find(Memo.class, id);

detach()

특정 Entity를 영속성 컨텍스트에서 분리한다.

em.detach(memo);

clear()

영속성 컨텍스트를 초기화한다.

em.clear();

관리하고 있던 모든 Entity가 준영속 상태가 된다.

close()

영속성 컨텍스트를 종료한다.

em.close();

merge()

준영속 또는 비영속 Entity를 다시 영속 상태로 병합한다.

Memo mergedMemo = em.merge(memo);

merge()의 반환값이 새롭게 영속 상태가 된 Entity라는 점이 중요하다.


11. Transaction

여러 DB 작업을 하나의 작업 단위로 묶는 개념

모든 작업이 성공하면 Commit, 하나라도 실패하면 Rollback한다.

Transaction 시작
      ↓
DB 작업
      ↓
성공 → COMMIT
실패 → ROLLBACK

@Transactional

Spring에서는 @Transactional을 사용하여 트랜잭션을 쉽게 적용할 수 있다.

@Transactional
public void updateMemo() {
    // DB 작업
}

정상적으로 실행되면 Commit하고, 예외가 발생하면 Rollback한다.

readOnly = true

조회 전용 트랜잭션에서 사용한다.

@Transactional(readOnly = true)

조회 작업에 대한 최적화가 가능하다.


12. 영속성 컨텍스트와 Transaction

Spring 환경에서는 트랜잭션과 영속성 컨텍스트의 생명주기가 연결되어 있다.

트랜잭션이 유지되는 동안 영속성 컨텍스트도 유지되기 때문에 JPA의 기능을 사용할 수 있다.

따라서 JPA에서 DB에 데이터를 저장, 수정, 삭제하려면 트랜잭션 적용이 필요하다.

조회는 단순 조회라면 트랜잭션이 필수는 아니지만, 상황에 따라 트랜잭션이 필요한 경우가 있다.


13. Transaction 전파

Spring에서는 Service에서 Repository까지 트랜잭션을 전달할 수 있도록 트랜잭션 전파 기능을 제공한다.

@Transactional의 기본 전파 옵션은 REQUIRED이다.

REQUIRED

부모 메서드에 트랜잭션이 존재하면 자식 메서드가 부모 트랜잭션에 참여한다.

Service
@Transactional
   ↓
Repository
@Transactional

→ 하나의 Transaction으로 동작

즉, 여러 계층에서 @Transactional이 사용되더라도 기존 트랜잭션이 있다면 새로운 트랜잭션을 만드는 것이 아니라 기존 트랜잭션에 합류한다.


14. Spring Boot에서 JPA 사용

Spring Boot에서는 JPA 설정을 보다 쉽게 할 수 있다.

의존성

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

application.properties

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

ddl-auto

옵션역할
create기존 테이블 삭제 후 생성
create-drop생성 후 애플리케이션 종료 시 삭제
update변경된 부분만 반영
validateEntity와 테이블 매핑 검증
none아무 작업도 하지 않음

개발 환경에서는 설정에 따라 편리하게 사용할 수 있지만, 운영 환경에서는 데이터 손실 가능성을 고려해야 한다.


15. Spring Data JPA

JPA를 쉽게 사용할 수 있도록 만들어진 Spring Data의 모듈

JPA를 직접 사용하면 EntityManager 등을 이용해 DB 작업을 처리해야 한다.

Spring Data JPA에서는 JpaRepository를 제공하여 반복적인 CRUD 코드를 줄일 수 있다.

public interface MemoRepository
        extends JpaRepository<Memo, Long> {
}

Spring Data JPA가 Repository의 구현체를 자동으로 생성하기 때문에 직접 구현 클래스를 작성할 필요가 없다.


16. JpaRepository 주요 메서드

save()

Entity 저장

memoRepository.save(memo);

findAll()

전체 데이터 조회

memoRepository.findAll();

findById()

ID를 기준으로 조회

memoRepository.findById(id);

반환 타입은 Optional이다.

memoRepository.findById(id)
    .orElseThrow(() ->
        new IllegalArgumentException("메모가 존재하지 않습니다.")
    );

delete()

Entity 삭제

memoRepository.delete(memo);

17. 변경 감지를 이용한 수정

Spring Data JPA에는 별도의 update() 메서드가 없다.

Entity를 조회한 후 값을 변경하면 변경 감지(Dirty Checking)를 통해 UPDATE SQL이 생성된다.

@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {

    Memo memo = findMemo(id);

    memo.update(requestDto);

    return id;
}

여기서 중요한 것은 @Transactional이다.

트랜잭션이 종료될 때 변경 감지가 수행되고 DB에 UPDATE가 반영된다.


18. JPA Auditing

생성 시간이나 수정 시간을 Entity마다 직접 관리하는 것은 번거롭다.

Spring Data JPA에서는 JPA Auditing을 통해 생성일과 수정일을 자동으로 관리할 수 있다.

@CreatedDate

Entity가 처음 생성될 때 시간을 자동으로 저장한다.

@LastModifiedDate

Entity가 수정될 때 수정 시간을 자동으로 변경한다.

@MappedSuperclass

공통으로 사용하는 필드를 여러 Entity가 상속받을 수 있도록 한다.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

그리고 Spring Boot 애플리케이션에 다음 설정을 추가한다.

@EnableJpaAuditing

Entity에서 상속받아 사용한다.

public class Memo extends Timestamped {
}

19. Query Methods

Spring Data JPA에서는 메서드 이름을 이용하여 SQL을 생성할 수 있다.

예를 들어

List<Memo> findAllByOrderByModifiedAtDesc();

위 메서드는 modifiedAt을 기준으로 내림차순 조회한다.

또한 조건을 메서드 이름에 표현할 수 있다.

List<Memo> findAllByContentsContainsOrderByModifiedAtDesc(
    String keyword
);

→ contents에 특정 keyword가 포함된 데이터를 조회하고 수정일 기준으로 내림차순 정렬

Query Methods의 장점

  • SQL 직접 작성 감소
  • Repository 구현 코드 감소
  • 메서드 이름만으로 조회 조건 표현 가능

20. 이번 주 핵심 정리

이번 주에 배운 내용을 흐름으로 정리하면 다음과 같다.

3 Layer Architecture
        ↓
Controller / Service / Repository 역할 분리
        ↓
IoC / DI
        ↓
Spring IoC Container
        ↓
Bean 관리
        ↓
JPA
        ↓
Entity
        ↓
영속성 컨텍스트
        ↓
1차 캐시 / 쓰기 지연 / 변경 감지
        ↓
Transaction
        ↓
Spring Data JPA
        ↓
JpaRepository
        ↓
Query Methods

꼭 기억할 키워드

  • 3 Layer Architecture

    • Controller
    • Service
    • Repository
  • IoC

    • 제어의 역전
  • DI

    • 의존성 주입
    • 생성자 주입 권장
  • Bean

    • Spring이 관리하는 객체
  • JPA

    • Java ORM 표준 명세
  • Hibernate

    • JPA 구현체
  • Entity

    • JPA가 관리하는 객체
  • 영속성 컨텍스트

    • Entity를 관리하는 공간
  • 1차 캐시

    • Entity 조회 성능 및 객체 동일성 보장
  • Dirty Checking

    • Entity 변경 감지 → UPDATE SQL 생성
  • flush

    • 영속성 컨텍스트의 변경 내용을 DB에 반영
  • Transaction

    • Commit / Rollback
  • @Transactional

    • Spring에서 트랜잭션 적용
  • Transaction Propagation

    • REQUIRED가 기본값
    • 기존 트랜잭션에 참여
  • Spring Data JPA

    • JPA 사용을 편리하게 해주는 모듈
  • JpaRepository

    • 기본 CRUD 기능 제공
  • JPA Auditing

    • 생성일 / 수정일 자동 관리
  • Query Methods

    • 메서드 이름으로 조회 조건 생성

마무리

이번 주에는 단순히 JPA 사용법만 배운 것이 아니라 Spring이 객체와 DB를 어떻게 관리하는지를 중심으로 학습했다.

특히 IoC → DI → Bean → JPA → 영속성 컨텍스트 → Transaction → Spring Data JPA가 서로 연결되어 있다는 점을 이해하는 것이 중요했다.

기존에는 Repository에서 직접 SQL을 작성했다면, Spring Data JPA를 사용하면서는 JpaRepository와 Query Methods를 통해 훨씬 적은 코드로 DB 작업을 처리할 수 있다.

다음에는 이번에 배운 내용을 실제 프로젝트에 적용하면서 Entity 설계와 Repository, Service 계층을 직접 구성해보는 것이 중요할 것 같다.

0개의 댓글