트랜잭션 전파

📌 하나의 트랜잭션이 다른 트랜잭션 내에서 어떻게 동작할지를 결정하는 규칙으로 여러 개의 트랜잭션이 포함된 시스템에서 특정 작업이 다른 작업에 어떻게 영향을 미칠지를 정의한다.

  • 현재 클래스의 트랜잭션과 다른 클래스의 트랜잭션을 교통정리 한다.

  • 트랜잭션이 여러 계층 또는 메서드에서 어떻게 처리될지 정의한다.(@Transactional)
  • propagation 속성을 통해 트랜잭션의 동작 방식을 제어할 수 있다.
  • 다양한 비즈니스 요구 사항에 맞춰 복잡한 트랜잭션 흐름을 유연하게 설계할 수 있도록 돕는다.
  • 데이터 무결성과 비지니스 로직의 안정성을 보장할 수 있다.

 

  • 코드 예시
    • REQUIRED(Default) 사용
@Service
@RequiredArgsConstructor
public class MemberService {
    
    private final PointPolicy pointPolicy;

    @Transactional
    public void signUp(Member member) {
        // 회원 등록
        memberRepository.save(member);

        // 포인트 지급
        pointPolicy.addPoints(member.getId(), 100);
    }
}

@Component
public class PointPolicy {
    public void addPoints(Long memberId, int points) {
        // 포인트 지급 로직
        pointRepository.save(new Point(memberId, points));
    }
}
  • signUp() 메서드에 @Transactional 을 통해 트랜잭션 설정
  • 하위 addPoints() 메서드에 트랜잭션이 전파된다.
  • 하위 메서드가 실패하면 롤백된다.
    • 포인트 지급 로직에서 문제가 발생해도 회원 등록은 롤백된다.
  • 트랜잭션 동작

 

  • 트랜잭션 전파 종류
    • propagation 속성
      1. REQUIRED(Default)
        • 기존 트랜잭션이 있다면 기존 트랜잭션을 사용한다.
        • 기존 트랜잭션이 없다면 트랜잭션을 새로 생성한다.
      2. REQUIRES_NEW
        • 항상 새로운 트랜잭션을 시작하고, 기존의 트랜잭션은 보류한다.
        • 두 트랜잭션은 독립적으로 동작한다.
      3. SUPPORTS
        • 기존 트랜잭션이 있으면 해당 트랜잭션을 사용한다.
        • 기존 트랜잭션이 없으면 트랜잭션 없이 실행한다.
      4. NOT_SUPPORTED
        • 기존 트랜잭션이 있어도 트랜잭션을 중단하고 트랜잭션 없이 실행된다.
      5. MANDATORY
        • 기존 트랜잭션이 반드시 있어야한다.
        • 트랜잭션이 없으면 실행하지 않고 예외를 발생시킨다.
      6. NEVER
        • 트랜잭션 없이 실행되어야 한다.
        • 트랜잭션이 있으면 예외를 발생시킨다.
      7. NESTED
        • 현재 트랜잭션 내에서 중첩 트랜잭션을 생성한다.
        • 중첩 트랜잭션은 독립적으로 롤백할 수 있다.
        • 기존 트랜잭션이 Commit되면 중첩 트랜잭션도 Commit 된다.

 

  • REQUIRES_NEW
    • 회원 가입과 동시에 회원에게 포인트를 지급해야 하는 경우
@Service
@RequiredArgsConstructor
public class MemberService {

    private final PointPolicy pointPolicy;

    @Transactional
    public void signUp(Member member) {
        // 회원 등록
        memberRepository.save(member);

        // 포인트 지급
        pointPolicy.addPoints(member.getId(), 100);
    }
}

@Component
public class PointPolicy {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addPoints(Long memberId, int points) {
        // 포인트 지급 로직
        pointRepository.save(new Point(memberId, points));
    }
    
}
  • 요구사항 : 포인트 지급에 **실패**해도 회원가입은 완료되어야 한다.
  • REQUIRES_NEW 설정(독립적인 트랜잭션 유지)
    • 포인트 지급 로직에서 문제가 발생해도 회원 등록은 롤백되지 않는다.
  • 트랜잭션 동작

 

 

트랜잭션

**트랜잭션(Transaction)**은 데이터베이스에서 작업의 논리적인 단위를 의미합니다. 트랜잭션은 하나의 작업 단위를 구성하며, 이 단위가 완전히 성공하거나 실패해야 데이터의 무결성을 보장합니다.


트랜잭션의 특징 (ACID 속성)

  1. 원자성 (Atomicity):
    • 트랜잭션 내의 모든 작업이 모두 성공하거나 모두 실패해야 합니다.
    • 일부 작업만 실행되고 나머지가 실패하면, 전체 작업을 취소(롤백)하여 데이터 일관성을 유지합니다.
  2. 일관성 (Consistency):
    • 트랜잭션이 성공적으로 완료되면, 데이터베이스가 항상 일관성 있는 상태로 유지됩니다.
    • 예: 은행 송금 시, 한 계좌에서 돈을 빼면 다른 계좌에 같은 금액이 추가되어야 함.
  3. 고립성 (Isolation):
    • 여러 트랜잭션이 동시에 실행될 경우, 각 트랜잭션은 서로 독립적으로 실행되어야 합니다.
    • 한 트랜잭션의 중간 결과가 다른 트랜잭션에 노출되지 않음.
  4. 지속성 (Durability):
    • 트랜잭션이 커밋된 후에는 영구적으로 데이터베이스에 반영되어야 합니다.
    • 서버가 중단되거나 시스템 장애가 발생해도 데이터는 손실되지 않습니다.

트랜잭션의 상태

  1. 활성 (Active):
    • 트랜잭션이 시작되고 작업이 진행 중인 상태.
  2. 부분 완료 (Partially Committed):
    • 트랜잭션의 마지막 명령이 실행되었지만, 아직 커밋되지 않은 상태.
  3. 완료 (Committed):
    • 트랜잭션이 성공적으로 완료되어 데이터베이스에 반영된 상태.
  4. 실패 (Failed):
    • 트랜잭션이 오류로 인해 중단된 상태.
  5. 철회 (Aborted):
    • 트랜잭션이 실패하거나 취소되어 롤백된 상태.

트랜잭션의 처리 과정

  1. 트랜잭션 시작:
    • 트랜잭션을 시작하여 작업 단위를 정의.
  2. 작업 실행:
    • 트랜잭션 내에서 여러 데이터베이스 작업(쿼리, 삽입, 업데이트, 삭제 등)을 실행.
  3. 커밋 또는 롤백:
    • 모든 작업이 성공하면 커밋하여 데이터베이스에 변경 사항을 반영.
    • 작업 중 오류가 발생하면 롤백하여 변경 사항을 취소.

Spring에서의 트랜잭션 관리

Spring은 프록시 기반 AOP를 사용하여 트랜잭션 관리를 제공합니다. Spring의 트랜잭션 관리 기능은 선언적 방식과 프로그래밍 방식으로 사용 가능합니다.

1. 선언적 트랜잭션 관리

Spring에서는 @Transactional 어노테이션을 사용하여 선언적으로 트랜잭션을 관리할 수 있습니다.

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
        emailService.sendWelcomeEmail(user); // 예외 발생 시 전체 롤백
    }
}
  • @Transactional이 붙은 메서드는 트랜잭션이 시작되고, 예외 발생 시 롤백됩니다.
  • 커밋은 메서드 실행이 성공적으로 종료되면 수행됩니다.

2. 프로그래밍 방식 트랜잭션 관리

프로그래밍 방식으로 PlatformTransactionManager를 사용하여 트랜잭션을 직접 관리할 수 있습니다.

@Service
public class UserService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createUser(User user) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            userRepository.save(user);
            emailService.sendWelcomeEmail(user);
            transactionManager.commit(status); // 성공 시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status); // 실패 시 롤백
        }
    }
}

트랜잭션 전파 수준 (Propagation)

트랜잭션 전파(Propagation)는 트랜잭션이 다른 메서드 호출 시 어떻게 동작할지 정의합니다.

전파 속성 설명

REQUIRED 기본값. 기존 트랜잭션이 있으면 참여하고, 없으면 새 트랜잭션 생성.
REQUIRES_NEW 항상 새 트랜잭션을 생성. 기존 트랜잭션은 일시 중단.
NESTED 중첩 트랜잭션을 생성. 롤백은 부모 트랜잭션과 독립적.
MANDATORY 기존 트랜잭션이 없으면 예외 발생.
SUPPORTS 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행.
NOT_SUPPORTED 항상 트랜잭션 없이 실행. 기존 트랜잭션은 일시 중단.
NEVER 트랜잭션 없이 실행하며, 기존 트랜잭션이 있으면 예외 발생.

트랜잭션 격리 수준 (Isolation)

트랜잭션 격리 수준은 동시에 실행되는 트랜잭션 간의 상호작용을 제어합니다.

격리 수준 설명

DEFAULT 데이터베이스의 기본 격리 수준을 따름.
READ_UNCOMMITTED 다른 트랜잭션이 커밋하지 않은 데이터도 읽을 수 있음. (Dirty Read 가능)
READ_COMMITTED 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음.
REPEATABLE_READ 같은 트랜잭션 내에서 동일 데이터를 반복적으로 읽어도 동일한 결과를 보장.
SERIALIZABLE 가장 높은 격리 수준. 트랜잭션을 순차적으로 실행하여 충돌 방지.

트랜잭션 롤백 조건

Spring의 @Transactional은 기본적으로 런타임 예외가 발생할 때 롤백합니다.

롤백 예외 설정

@Transactional(rollbackFor = Exception.class)
public void process() {
    // Exception 발생 시 롤백
}

롤백 제외 설정

@Transactional(noRollbackFor = CustomException.class)
public void process() {
    // CustomException 발생 시 롤백하지 않음
}

트랜잭션의 장점

  1. 데이터 무결성 보장:
    • 작업 도중 오류가 발생해도 데이터 손상 방지.
  2. 동시성 제어:
    • 여러 사용자가 동시에 데이터베이스에 접근해도 데이터 충돌 방지.
  3. 복잡한 작업 관리:
    • 여러 작업 단위를 하나의 트랜잭션으로 묶어 관리 가능.
  4. 자동화된 관리:
    • Spring의 트랜잭션 관리를 통해 선언적으로 간단하게 설정 가능.

정리

**트랜잭션(Transaction)**은 하나의 논리적인 작업 단위를 정의하며, 데이터베이스의 무결성과 일관성을 보장합니다. Spring은 선언적 트랜잭션(@Transactional)을 제공하여 개발자가 쉽게 트랜잭션을 관리할 수 있도록 돕습니다.

핵심 개념:

  1. ACID 속성: 원자성, 일관성, 고립성, 지속성.
  2. Spring의 지원: 선언적(@Transactional), 프로그래밍 방식.
  3. 전파와 격리 수준: 트랜잭션의 실행 방식을 제어.

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] 쿼리 파일 만들기 (QueryMapper)  (0) 2025.01.26
[JPA] 데이터베이스 연결 (Driver)  (1) 2025.01.25
[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15
[JPA] Proxy  (0) 2025.01.14
[JPA] 상속관계 매핑  (0) 2025.01.13

Cascade

📌 영속성 전이(Cascade)란 JPA에서 특정 엔티티를 저장, 삭제 등의 작업을 할 때 연관된 엔티티에도 동일한 작업을 자동으로 적용하도록 설정하는 기능이다.

  • 영속성 전이는 지연 로딩, 즉시 로딩과는 아무 관련이 없다.

코드 예시

  • 1:N, N:1 양방향 연관관계
@Entity
@Table(name = "category")
public class Category {

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

    private String name;

    @OneToMany(mappedBy = "category")
    private List<Product> productList = new ArrayList<>();

    public Category() {
    }

    public Category(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void addProduct(Product product) {
        productList.add(product);
        product.setCategory(this);
    }

}
@Entity
@Table(name = "product")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;

    public Product() {
    }

    public Product(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setCategory(Category category) {
        this.category = category;
    }
}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Category category = new Category("food");
            em.persist(category);

            Product product1 = new Product("pizza");
            Product product2 = new Product("kimchi");
            category.addProduct(product1);
            category.addProduct(product2);

            em.persist(product1);
            em.persist(product2);


            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

실행결과

 

  • 총 세번의 INSERT SQL이 실행된다.

 

  • cascade 속성 적용
    • CascadeType.ALL
@Entity
@Table(name = "category")
public class Category {

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

    private String name;

    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
    private List<Product> productList = new ArrayList<>();

    public Category() {
    }

    public Category(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void addProduct(Product product) {
        productList.add(product);
        product.setCategory(this);
    }

}
public class CascadeMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Category category = new Category("food");

            Product product1 = new Product("pizza");
            Product product2 = new Product("kimchi");
            category.addProduct(product1);
            category.addProduct(product2);

            em.persist(category);

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

실행결과

em.persist(category) 만 해도 세번의 INSER SQL이 실행된다.

 

 

사용 방법과 주의점

📌 영속성 전이는 연관관계 매핑과는 아무 관련이 없다.

 

  • 영속성 전이(Cascade)
    • 단순히 Entity를 저장, 삭제할 때 연관된 Entity에도 동일한 작업을 적용한다.
    • 속성 종류
      1. ALL : 모두 적용
      2. PERSIST : 영속
      3. REMOVE : 삭제
      4. MERGE
      5. REFRESH
      6. DETACH
  • 사용 방법
    • 단일 Entity에 완전히 종속적인 경우 생명주기가 같다면 사용한다.
      • 블로그 글의 댓글처럼 항상 글을 통해서만 관리하는 경우
      • 상품과 상품 이미지처럼 특정 상품에 종속되어 관리되는 경우
    • 작가와 책
      • 책은 특정 작가와 연관되지만 작가가 활동하지 않아도 책은 보존된다.
      • 이런 경우는 사용하지 않는다.

 

고아 객체

📌 JPA에서 부모 엔티티와의 연관관계가 끊어진 자식 엔티티를 말한다.

  • 고아 객체 삭제
    • 부모 엔티티와 연관관계가 끊어진 자식 Entity를 자동으로 삭제한다.
    • orphanRemoval = true 사용
      • 기본 값 : false
@Entity
@Table(name = "category")
public class Category {

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

    private String name;

    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Product> productList = new ArrayList<>();

    public Category() {
    }

    public Category(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
    
    public List<Product> getProductList() {
        return productList;
    }

    public void addProduct(Product product) {
        productList.add(product);
        product.setCategory(this);
    }

}
public class OrphanRemovalMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Category category = new Category("food");

            Product product1 = new Product("pizza");
            Product product2 = new Product("kimchi");
            category.addProduct(product1);
            category.addProduct(product2);

            em.persist(category);

            em.flush();
            em.clear();

            Category findCategory = em.find(Category.class, category.getId());
            findCategory.getProductList().remove(0);

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
  • 실행결과

  • Collection에서 제거된 객체는 삭제된다.

 

  • 주의점
    1. 참조하는 곳이 하나인 경우에만 사용한다.
      • 단일 Entity에 완전히 종속적인 경우 생명주기가 같다면 사용한다.
    2. @OneToOne, @OneToMany만 사용이 가능하다.
    • 부모 Entity를 제거하면 자식 Entity는 고아 객체가 된다.
    • CascadeType.REMOVE와 비슷하게 동작한다.

 

CascadeType.ALL 제거

@Entity
@Table(name = "category")
public class Category {

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

    private String name;

		@OneToMany(mappedBy = "category", orphanRemoval = true)
    private List<Product> productList = new ArrayList<>();

    public Category() {
    }

    public Category(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Product> getProductList() {
        return productList;
    }

    public void addProduct(Product product) {
        productList.add(product);
        product.setCategory(this);
    }

}
public class OrphanRemovalMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Category category = new Category("food");

            Product product1 = new Product("pizza");
            Product product2 = new Product("kimchi");
            category.addProduct(product1);
            category.addProduct(product2);

            em.persist(category);
            // cascade 제거
            em.persist(product1);
            em.persist(product2);

            em.flush();
            em.clear();

            // cascade 제거
            Category findCategory = em.find(Category.class, category.getId());
            em.remove(findCategory);


            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

실행결과

  • Collection이 제거되었기 때문에 모두 삭제한다.

 

Lazy Loading

📌 지연 로딩(Lazy Loading)은 데이터를 실제로 사용할 때 데이터베이스에서 조회하는 방식

 

JPA의 지연로딩

  • fetch 속성 사용
    • FetchType.LAZY : 지연로딩
  • 지연로딩을 사용하면 Proxy 객체를 조회한다.
  • 연관된 객체(Company)를 매번 함께 조회하는것은 낭비인 경우에 사용한다.
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;
		
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
public class FetchTypeLazyMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Company company = new Company("sparta");
            em.persist(company);

            Tutor tutor = new Tutor("wonuk");
            tutor.setCompany(company);
            em.persist(tutor);

            // 영속성 컨텍스트 초기화
            em.flush();
            em.clear();

            // em.find()
            Tutor findTutor = em.find(Tutor.class, tutor.getId());

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

Tutor만 조회한다.

 

getCompany()

System.out.println("findTutor.getCompany().getClass() = " + findTutor.getCompany().getClass());

 

실행결과

Proxy로 조회한다.

 

getCompany().getName()

System.out.println("findTutor.getCompany().getName() = " + findTutor.getCompany().getName());

 

실행결과

  • 실제 값에 접근할 때 조회 SQL이 실행된다.
  • 실제 Company 의 값을 사용하는 시점에 초기화(DB 조회)된다.
  • 지연 로딩을 사용하면 연관된 객체를 Proxy로 조회한다.

 

Eager Loading

📌 즉시 로딩(Eager Loading)은 엔티티를 조회할 때 연관된 데이터까지 모두 한 번에 로드하는 방식

 

  • JPA의 즉시 로딩
    • fetch 속성 사용
      • FetchType.EAGER : 즉시 로딩
    • Proxy 객체를 조회하지 않고 한 번에 연관된 객체까지 조회한다.
    • 연관된 객체(Company)를 매번 함께 조회하는것이 효율적인 경우에 사용한다.
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
public class FetchTypeEagerMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Company company = new Company("sparta");
            em.persist(company);

            Tutor tutor = new Tutor("wonuk");
            tutor.setCompany(company);
            em.persist(tutor);

            // 영속성 컨텍스트 초기화
            em.flush();
            em.clear();

            // em.find()
            Tutor findTutor = em.find(Tutor.class, tutor.getId());

            // getCompany()
            System.out.println("findTutor.getCompany().getClass() = " + findTutor.getCompany().getClass());

            // getCompany().getName()
            System.out.println("findTutor.getCompany().getName() = " + findTutor.getCompany().getName());

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
    
}

JOIN을 사용해 한번의 SQL로 모두 조회하기 때문에 Proxy가 필요없다.

 

 

즉시 로딩 주의점

 

 

코드 예시(N+1 문제)

List<Tutor> tutorList = em.createQuery("select t from Tutor t", Tutor.class).getResultList();

 

실행결과

  • 조회 SQL이 N+1번 실행된다.
    • 처음 실행된 최초 SQL Query : 1(Tutor)
    • 연관된 객체 조회 SQL Query : N(Company)
  • JPQL은 SQL이 그대로 변환되어 조회된 Tutor 만큼 EAGER로 설정된 Company가 함께 조회된다.
  • em.find() 는 JPA가 내부적으로 최적화한다.

N+1 문제 해결 방법

  • 모든 연관관계를 LAZY로 설정한다.
  1. JPQL fetch join : Rumtime에 원하는 Entity를 함께 조회할 수 있다.(대부분 사용)
  2. @EntityGraph
  3. @BatchSize
  4. Native Query

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] 데이터베이스 연결 (Driver)  (1) 2025.01.25
[JPA] JPA와 Transaction  (0) 2025.01.17
[JPA] Proxy  (0) 2025.01.14
[JPA] 상속관계 매핑  (0) 2025.01.13
[JPA] 연관관계  (0) 2025.01.12

Entity 조회

📌 em.getReference()는 JPA의 EntityManager에서 제공하는 메서드로 특정 엔티티의 프록시 객체를 반환한다. 지연 로딩(Lazy Loading)을 활용해 데이터베이스 조회를 미루고 실제로 엔티티의 속성에 접근할 때만 데이터베이스를 조회하도록 한다.

 

@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "company_id")
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
    
    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }
}
// 1. tutor의 company를 함께 조회하는 경우
Tutor findTutor = em.find(Tutor.class, 1L);

String tutorName = findTutor.getName();
Company tutorCompany = findTutor.getCompany();

System.out.println("tutorName = " + tutorName);
System.out.println("tutorCompany.getName() = " + tutorCompany.getName());
// 2. tutor만 조회하는 경우
Tutor findTutor = em.find(Tutor.class, 1L);

String tutorName = findTutor.getName();

System.out.println("tutorName = " + tutorName);
  1. Tutor를 조회할 때 Company 를 함께 조회
    • Company를 매번 함께 조회하는것은 낭비이다.
  2. Tutor만 조회
    • Company 조회를 위해 추가적인 조회 SQL이 실행되어야 한다.
  • 이때 프록시를 사용하여 효율적으로 관리할 수 있다.

 

 

Proxy

📌 JPA에서 엔티티 객체의 지연 로딩(Lazy Loading)을 지원하기 위해 사용하는 대리 객체로 실제 엔티티 객체를 생성하거나 데이터베이스에서 값을 읽어오지 않고도 엔티티의 참조를 사용할 수 있다.

  • 대리자 또는 중간 대리 객체를 의미

  • 데이터베이스 조회를 지연하는 가짜(Proxy) 객체를 조회한다.
    • 실제 Entity와 == 비교 실패, instanceof 사용
  • target : 진짜 객체의 참조를 보관한다.

 

Proxy 객체 초기화

  1. em.getReference() : 프록시 객체 조회
  2. getName() : 프록시 객체의 getName() 호출
  3. JPA가 영속성 컨텍스트에 target 초기화 요청
  4. 실제 DB 조회
  5. Entity 생성
  6. target의 getName() 호출

 

Proxy 특징

  • 최초로 사용(실제 Entity에 접근)할 때 한 번만 초기화된다.
  • 프록시 객체를 통해 실제 Entity에 접근할 수 있다.
  • em.getReference() 호출 시 영속성 컨텍스트에 Entity가 존재하면 실제 Entity가 반환된다.
  • 준영속 상태에서 프록시를 초기화하면 LazyInitializationException 예외가 발생한다.
Tutor proxyTutor = em.getReference(Tutor.class, tutor.getId());
System.out.println("proxyTutor.getClass() = " + proxyTutor.getClass());

// 준영속 상태
em.detach(proxyTutor);

proxyTutor.getName();

 

실행결과

  • detach() : 영속성 컨텍스트가 관리하지 않는다.
  • 영속성 컨텍스트를 통해 도움을 받아야만 실제 Entity에 접근이 가능하다.
  • 실제 JPA 개발에서 가장 많이 마주치는 Exception

 

영속성 컨텍스트 (Persistence Context)

영속성 컨텍스트는 JPA(Java Persistence API)에서 엔티티 객체를 **영구 저장소(데이터베이스)**에 저장하거나 관리하는 중간 작업 영역을 의미합니다. 쉽게 말해, 엔티티 객체를 관리하는 JPA의 메모리 공간입니다.


영속성 컨텍스트의 주요 특징

  1. 엔티티 관리:
    • 영속성 컨텍스트는 엔티티 객체를 영속성 상태로 관리합니다.
    • 관리 중인 엔티티는 변경 사항이 자동으로 데이터베이스에 반영됩니다.
  2. 1차 캐시:
    • 영속성 컨텍스트는 엔티티를 1차 캐시에 저장하여, 동일한 엔티티를 데이터베이스에서 다시 조회하지 않도록 최적화합니다.
  3. 엔티티 동일성 보장:
    • 동일한 영속성 컨텍스트 내에서는 동일한 엔티티 객체를 공유(동일성 보장)합니다.
  4. 변경 감지:
    • 영속성 컨텍스트는 관리 중인 엔티티의 변경 사항을 감지하여 데이터베이스에 자동으로 반영합니다.
  5. 쓰기 지연:
    • 트랜잭션 커밋 시점에 변경 사항을 한꺼번에 데이터베이스에 반영하여 성능을 최적화합니다.
  6. 지연 로딩:
    • 필요한 시점에만 데이터베이스에서 데이터를 로드하여 효율적으로 자원을 사용합니다.

영속성 컨텍스트의 상태

  1. 비영속 (Transient):
    • 영속성 컨텍스트에서 관리되지 않는 상태.
    • 데이터베이스와 전혀 관련이 없는 상태.
    User user = new User(); // 비영속 상태
    user.setName("John");
    
  2. 영속 (Persistent):
    • 영속성 컨텍스트에 의해 관리되는 상태.
    • 데이터베이스와 연동되며 변경 사항이 자동으로 반영됩니다.
    User user = new User();
    user.setName("John");
    entityManager.persist(user); // 영속 상태
    
  3. 준영속 (Detached):
    • 영속성 컨텍스트에서 관리되지 않지만, 이전에 영속 상태였던 엔티티.
    • 데이터베이스와 연동되지 않습니다.
    entityManager.detach(user); // 준영속 상태
    
  4. 삭제 (Removed):
    • 삭제 요청이 되어 데이터베이스에서 삭제될 예정인 상태.
    • 트랜잭션이 커밋되면 데이터베이스에서 삭제됩니다.
    entityManager.remove(user); // 삭제 상태
    

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

1. 1차 캐시

  • 영속성 컨텍스트는 엔티티를 1차 캐시에 저장합니다.
  • 동일한 엔티티를 반복적으로 조회할 경우, 데이터베이스 대신 1차 캐시에서 데이터를 가져옵니다.
User user1 = entityManager.find(User.class, 1L); // DB 조회
User user2 = entityManager.find(User.class, 1L); // 1차 캐시에서 조회

2. 엔티티 동일성 보장

  • 동일한 영속성 컨텍스트에서는 동일한 엔티티 객체를 반환합니다.
User user1 = entityManager.find(User.class, 1L);
User user2 = entityManager.find(User.class, 1L);

System.out.println(user1 == user2); // true (같은 객체)

3. 변경 감지 (Dirty Checking)

  • 엔티티의 필드 값이 변경되면 영속성 컨텍스트가 이를 감지하고, 트랜잭션 커밋 시점에 데이터베이스에 반영합니다.
User user = entityManager.find(User.class, 1L);
user.setName("Updated Name"); // 변경 감지

entityManager.flush(); // 변경 사항 반영

4. 쓰기 지연 (Write-Behind)

  • 데이터베이스에 변경 사항을 즉시 반영하지 않고, 트랜잭션 커밋 시점에 한꺼번에 반영합니다.
entityManager.persist(user1);
entityManager.persist(user2);

// SQL 실행은 트랜잭션 커밋 시점에 발생
transaction.commit();

5. 지연 로딩 (Lazy Loading)

  • 연관된 엔티티를 실제로 사용할 때까지 데이터베이스에서 로드하지 않습니다.
User user = entityManager.find(User.class, 1L);
List<Order> orders = user.getOrders(); // 이 시점에 DB에서 로드

영속성 컨텍스트의 동작 예시

엔티티 상태 전환

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
}

// 비영속 상태
User user = new User();
user.setName("John");

// 영속 상태
entityManager.persist(user);

// 준영속 상태
entityManager.detach(user);

// 삭제 상태
entityManager.remove(user);

트랜잭션과 영속성 컨텍스트

  1. 영속성 컨텍스트는 트랜잭션 범위 내에서 관리됩니다.
    • 트랜잭션이 종료되면 영속성 컨텍스트도 종료됩니다.
  2. 트랜잭션이 커밋되면 데이터베이스와 동기화됩니다.
    • 엔티티의 변경 사항이 데이터베이스에 반영됩니다.

영속성 컨텍스트의 장점

  1. 성능 최적화:
    • 1차 캐시와 쓰기 지연을 통해 데이터베이스 접근 횟수를 줄임.
  2. 변경 감지:
    • 엔티티의 변경 사항을 자동으로 반영하여 코드 단순화.
  3. 지연 로딩:
    • 필요한 시점에만 데이터를 로드하여 리소스 사용을 최소화.
  4. 엔티티 동일성 보장:
    • 동일한 트랜잭션 내에서 같은 데이터를 일관되게 처리 가능.

영속성 컨텍스트를 활용한 주요 작업 흐름

  1. 엔티티 생성 및 영속화:
    • 새 엔티티 객체를 생성하고, 영속성 컨텍스트에 추가.
  2. 엔티티 조회:
    • 데이터베이스에서 데이터를 가져와 영속성 컨텍스트에 저장.
  3. 변경 사항 감지:
    • 엔티티의 필드 값이 변경되면 자동으로 감지하여 반영.
  4. 엔티티 삭제:
    • 영속성 컨텍스트에서 엔티티를 제거하고, 데이터베이스에서도 삭제.

정리

영속성 컨텍스트는 JPA가 엔티티 객체를 관리하고, 데이터베이스와의 상호작용을 효율적으로 처리하기 위해 사용하는 핵심 메커니즘입니다.

  • 핵심 기능: 1차 캐시, 변경 감지, 쓰기 지연, 지연 로딩.
  • 장점: 성능 최적화, 코드 단순화, 데이터 일관성 보장.

쉽게 말해, 영속성 컨텍스트는 JPA가 엔티티 객체를 "관리"하고 "동기화"하는 공간입니다!

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] JPA와 Transaction  (0) 2025.01.17
[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15
[JPA] 상속관계 매핑  (0) 2025.01.13
[JPA] 연관관계  (0) 2025.01.12
[JPA] Spring Data JPA  (0) 2025.01.07

테이블 전략

📌 JPA에서 엔티티 상속 구조를 데이터베이스 테이블에 매핑하는 방법을 말한다. JPA는 엔티티의 상속 구조를 처리하기 위해 3가지의 테이블 전략을 제공하며 각각의 전략은 데이터 저장 방식과 성능에 차이가 있으므로 프로젝트의 요구사항에 맞게 선택할 수 있다.

  • 관계형 데이터베이스의 테이블에는 상속 관계가 없다.

 

dtype

@DiscriminatorColumn의 dtype은 **JPA (Java Persistence API)**에서 **싱글 테이블 상속 전략(Single Table Inheritance)**을 사용할 때, 엔티티의 타입을 구분하는 데 사용되는 컬럼을 의미합니다. dtype 컬럼은 테이블 내의 데이터가 어떤 엔티티 타입에 해당하는지 식별하기 위해 자동으로 생성됩니다.

 

+ 엔티티 : 데이터베이스 테이블과 매핑되는 자바 클래스


@DiscriminatorColumn의 역할

  1. 싱글 테이블 상속 전략에서 엔티티 유형을 구분하기 위해 사용.
    • 싱글 테이블 상속 전략: 하나의 테이블에 상속 관계를 가진 모든 엔티티 데이터를 저장.
    • 각 엔티티 타입을 식별할 수 있도록 테이블에 구분 컬럼(Discriminator Column)이 필요.
  2. 기본적으로 DiscriminatorColumn의 이름은 **DTYPE**이며, 이는 JPA의 관례입니다.
  3. @DiscriminatorColumn을 사용하면 이 컬럼의 이름이나 기타 설정을 변경할 수 있습니다.

기본 dtype 컬럼의 생성

예를 들어, 다음과 같은 상속 구조가 있다고 가정합니다.

엔티티 상속 구조

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
public abstract class Animal {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

@Entity
@DiscriminatorValue("DOG")
public class Dog extends Animal {
    private String breed;
}

@Entity
@DiscriminatorValue("CAT")
public class Cat extends Animal {
    private int livesLeft;
}

결과 테이블

이 코드로 인해 데이터베이스에 다음과 같은 테이블이 생성됩니다:

ID NAME dtype BREED LIVESLEFT

1 Max DOG Husky NULL
2 Whiskers CAT NULL 9
  • dtype: 데이터를 어떤 엔티티(Dog, Cat)로 매핑할지 식별하기 위한 컬럼.
  • DiscriminatorValue로 지정한 값이 dtype 컬럼에 저장됩니다.

@DiscriminatorColumn의 주요 속성

  • name:
    • dtype 컬럼의 이름을 설정합니다.
    • 기본값: "DTYPE".
  • length:
    • dtype 컬럼의 길이를 설정합니다.
    • 기본값: 31.
  • discriminatorType:
    • 컬럼의 데이터 타입을 설정합니다.
    • 기본값: STRING.
    • 다른 옵션: CHAR, INTEGER.

예: 이름과 타입 변경

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "animal_type", discriminatorType = DiscriminatorType.STRING, length = 50)
public abstract class Animal {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

결과:

ID NAME animal_type BREED LIVESLEFT

1 Max DOG Husky NULL
2 Whiskers CAT NULL 9

Discriminator 컬럼이 필요한 이유

  • JPA는 테이블의 데이터를 읽어 올 때, dtype 컬럼의 값을 기준으로 어떤 서브클래스(엔티티)로 변환할지 결정합니다.
  • @DiscriminatorValue에 지정된 값을 사용하여 데이터를 매핑합니다.

기본값: DTYPE

  • JPA는 관례적으로 **DTYPE**이라는 이름을 기본값으로 사용합니다.
  • @DiscriminatorColumn을 명시하지 않으면 DTYPE 컬럼이 자동으로 생성됩니다.

정리

  • dtype 컬럼:
    • 싱글 테이블 상속 전략에서 엔티티 타입을 식별하기 위한 컬럼.
    • JPA의 기본 관례로 이름은 DTYPE.
  • @DiscriminatorColumn:
    • dtype 컬럼의 이름, 데이터 타입, 길이 등을 변경할 수 있는 어노테이션.
  • 사용 이유:
    • 테이블 내 데이터와 엔티티 타입 간의 매핑을 정확히 하기 위해 필요.

 

JPA의 테이블 전략

📌 JPA는 모든 전략으로 테이블을 구현할 수 있도록 지원한다.

 

Annotation

  • @Inheritance(strategy = InheritanceType.${전략})
    1. JOINED : 조인
    2. SINGLE_TABLE : 단일 테이블(Default)
    3. TABLE_PER_CLASS : 구현 클래스
  • @DiscriminatorColumn(name = "dtype")
    • dtype 컬럼을 생성한다(관례).
    • 이름 변경이 가능하다.
    • 기본 값 : DTYPE
  • @DiscriminatorValue("${값}")
    • dtype 값 지정
    • 기본 값 : 클래스 이름
@Entity
@Table(name = "product")
@DiscriminatorColumn(name = "dtype")
public abstract class Product {

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

    private String name;

    private BigDecimal price;

    public Product() {
    }

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

}
@Entity
@Table(name = "book")
@DiscriminatorValue(value = "B")
public class Book extends Product {

    private String author;

    public Book() {
    }

    public Book(String author, String name, BigDecimal price) {
        super(name, price);
        this.author = author;
    }
}
@Entity
@Table(name = "coat")
@DiscriminatorValue(value = "C")
public class Coat extends Product {

    private Integer size;

}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Book book = new Book("wonuk", "spring-advanced", BigDecimal.TEN);
            em.persist(book);

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

 

JOINED

@Entity
@Table(name = "product")
@DiscriminatorColumn(name = "dtype")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Product {

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

    private String name;

    private BigDecimal price;

    public Product() {
    }

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }
}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Book book = new Book("wonuk", "spring-advanced", BigDecimal.TEN);
            em.persist(book);

            em.flush();
            em.clear();

            Book findBook = em.find(Book.class, book.getId());


            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

 

TABLE_PER_CLASS

@Entity
@Table(name = "product")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Product {

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

    private String name;

    private BigDecimal price;

    public Product() {
    }

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }
}

 

 

TABLE_PER_CLASS 문제점

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {

            Book book = new Book("wonuk", "spring-advanced", BigDecimal.TEN);
            em.persist(book);

            em.flush();
            em.clear();

            Product findProduct = em.find(Product.class, book.getId());

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }

 

테이블 전략 장단점

  • JOINED
    • 장점
      1. 테이블 정규화
      2. 외래 키 참조 무결성
      3. 저장공간 효율
    • 단점
      1. 조회시 JOIN을 많이 사용한다.
      2. 데이터 저장시 INSERT SQL 이 2번 호출된다.
      3. SQL Query가 복잡하여 성능이 저하될 수 있다.
  • SINGLE_TABLE
    • 장점
      1. JOIN을 사용하지 않는다.
      2. 실행되는 SQL이 단순하다.
    • 단점
      1. 자식 Entity가 매핑한 컬럼은 모두 null을 허용한다.
      2. 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
      3. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
  • TABLE_PER_CLASS
    • 테이블끼리 연관짓기 힘들다, 사용하지 않는것을 권장한다.
    • 장점
      1. 자식 클래스를 명확하게 구분해서 처리할 수 있다.
      2. not null 제약조건 사용이 가능하다.
    • 단점
      1. 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
      2. 부모 객체 타입으로 조회할 때 모든 테이블을 조회해야 한다.
  • 선택 기준
    • 비지니스적으로 복잡하다 = JOINED
      • 객체 지향적인 개발에 어울리는 방법
    • 단순하고 확장 가능성이 없다 = SINGLE_TABLE
    • 두 방법의 장단점을 구분하여 상황에 맞는 선택을 해야한다.

 

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] 지연로딩, 즉시로딩  (0) 2025.01.15
[JPA] Proxy  (0) 2025.01.14
[JPA] 연관관계  (0) 2025.01.12
[JPA] Spring Data JPA  (0) 2025.01.07
[JPA] 연관관계 Mapping  (0) 2025.01.06

연관관계 매핑

📌 JPA 연관관계 매핑을 통해 데이터베이스 테이블 간의 관계를 객체 지향적으로 표현하여 엔티티 클래스들 간의 관계를 설정한다. JPA를 통해 연관관계를 매핑하면 SQL을 직접 작성하지 않고도 객체 간의 관계를 활용하여 쉽게 데이터를 조회하고 조작할 수 있다.

 

 

1 : N 단방향

📌 한 엔티티가 @OneToMany를 통해 여러 엔티티와 관계를 맺는 경우를 말한다. 이 경우 연관관계의 주인은 1에서 가지고 있다.

  • 1의 Entity가 외래 키(FK)를 관리한다. (연관관계의 주인)
  • DB 입장에서는 항상 외래 키가 N 쪽에 위치해야 한다. (설계상 불가)
    • 1(Company)이 N(Tutor)의 외래 키를 수정해야 한다.
더보기
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

}
@Entity
@Table(name = "company")
public class Company {

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

    private String name;

    @OneToMany
    @JoinColumn(name = "company_id")
    private List<Tutor> tutors = new ArrayList<>();

    public Company() {
    }

    public Company(String name) {
        this.name = name;
    }

    public List<Tutor> getTutors() {
        return tutors;
    }
}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");

        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();

        transaction.begin();

        try {
						// Tutor 생성 및 persist
            Tutor tutor = new Tutor("wonuk" );
            em.persist(tutor);
						
						// Company 생성 및 persist
            Company company = new Company("sparta");
            // Tutor 테이블에 추가
            company.getTutors().add(tutor);
            em.persist(company);

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

@JoinColumn 미사용

@Entity
@Table(name = "company")
public class Company {

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

    private String name;

    @OneToMany
//    @JoinColumn(name = "company_id") 주석처리
    private List<Tutor> tutors = new ArrayList<>();

    public Company() {
    }

    public Company(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Tutor> getTutors() {
        return tutors;
    }
}
  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)

  • @JoinColumn 을 사용하지 않으면 중간 테이블 방식을 사용한다.
    • 사용 필수

 

 

1 : N 양방향

📌 양방향 연관 관계는 하나의 엔티티가 다른 엔티티와 관계를 맺고 그 반대 방향에서도 서로 참조가 가능하도록 설정한 관계이다.

 

  • Tutor의 참조용 객체 Company는 읽기 전용 매핑이어야 한다.
  • 만약, 양쪽에서 수정이 가능하면 예측이 불가능해진다.
  • 1:N 양방향 연관관계의 주인은 Company 이다.
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "company_id", insertable = false, updatable = false)
    private Company company;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
  • 연관관계의 주인이 되지 않도록 insertable = false, updatable = false 설정
  • 1:N 단방향과 같은 이유로 N:1 양방향을 쓰면된다.

 

1 : 1 단방향

📌 두 Entity가 @OneToOne 을 통해 서로 관계를 맺는 경우를 말한다.

  • 외래 키(FK)의 주인을 선택할 수 있다.(Tutor로 가정)
  • 외래 키에 유니크 제약조건이 필요하다.(1:1)
@Entity
@Table(name = "address")
public class Address {

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

    private String name;

}
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }
    
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

 

  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)

 

대상 테이블에 외래 키

  • Tutor Entity의 Address로 Address 테이블의 tutor_id는 관리하지 못한다.
  • 애초에 JPA가 지원하지 않는다.

 

1 : 1 양방향

  • N:1 양방향 연관관계와 유사하다.
  • 외래 키(FK)가 있는 곳이 연관관계의 주인
@Entity
@Table(name = "address")
public class Address {

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

    private String name;

    // 읽기 전용
    @OneToOne(mappedBy = "address")
    private Tutor tutor;

}
  • 실행결과는 N:1의 양방향과 같이 읽기 전용으로 동작한다.

대상 테이블에 외래 키 양방향

  • 1:1연관관계에서 자신이 가지고 있는 외래 키는 자신이 관리해야 한다.
  • Tutor Entity의 Address 는 참조(읽기 전용)객체 이다.

 

1 : 1 연관관계 외래 키

📌 1:1 연관관계에서 외래 키는 양쪽 모두가 관리할 수 있다.

 

  • 1:1 연관관계 외래 키
    • 둘중 어떤 테이블을 사용해도 무방하다.
    • 단, 테이블은 한번 만들어지면 변경이 어렵다.
  • 요구사항 변경
    • 한명의 Tutor가 여러개의 Address를 가질 수 있다.
    • 기존 테이블

수정 테이블

  • UNIQUE 제약조건만 지우면 된다.
  • N:1 연관관계로 자연스럽게 변경이 가능하다.
  • 단, 양방향으로 만들어야한다.

 

개발자 관점

  • Tutor 테이블에서 접근할 확률이 높으니 Tutor 테이블이 외래 키(FK)를 가지면 편하다.
  • 성능상 이점이 생긴다.
  • 명확한 1:1 연관관계라면 해당 방법을 선택하면 된다.
  • 결국 여러가지 방법 중 장점과 단점을 비교하여 선택하면 된다.

 

외래 키 위치 장단점

  1. 주 테이블
    • 장점
      • JPA로 객체 지향적인 개발이 가능해진다.
      • 주 테이블만 조회해도 대상 테이블을 조회할 수 있다.
    • 단점
      • 대상 테이블에 값이 없다면 null이 허용된다.
        • 데이터 무결성을 지키지 못한다.
      • 대상 테이블의 데이터를 참조하기 때문에 삭제될 때 외래 키 값을 처리하는 관리 필요
  2. 대상 테이블
    • 장점
      • 데이터베이스 무결성 보장
      • 주 테이블과 대상 테이블의 연관관계 변경 시 테이블 구조가 유지된다.
    • 단점
      • 조회 성능이 떨어진다.
      • 연관관계 매핑 설정이 복잡하다.
      • 지연 로딩으로 설정해도 즉시 로딩된다.(중요)

 

N : M 연관관계

📌 두 Entity가 @ManyToMany를 통해 서로 다수의 관계를 가진다.

  • 조심해서 사용해야 한다.

 

N : M 단방향

@Entity
@Table(name = "language")
public class Language {

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

    private String name;

}
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    @ManyToMany
    @JoinTable(
            name = "tutor_language",
            joinColumns = @JoinColumn(name = "tutor_id"),
            inverseJoinColumns = @JoinColumn(name = "language_id")
    )
    private List<Language> languages = new ArrayList<>();

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)

 

 

N : M 양방향

@Entity
@Table(name = "language")
public class Language {

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

    private String name;

    @ManyToMany(mappedBy = "languages")
    private List<Tutor> tutors = new ArrayList<>();

}

@OneToOne 양방향 처럼 동작하지만 중간 테이블이 생성된다.

 

 

N : M 매핑의 문제점

📌 @ManyToManyN:M 연관관계 설정을 하게되면 편리해 보이지만 실제로 사용하기 까다롭다.

 

  1. 실제 설계에서는 level, license 와 같은 추가적인 데이터가 필요하다.
    • @ManyToMany에서 사용할 수 없다.
  2. 중간 테이블이 숨겨져 있어서 생각하지 못한 SQL Query가 실행된다.
  3. tutor_id, language_id 를 묶어서 PK로 설정된다.
    • PK가 종속적이면 사이드 이펙트가 생긴다.
    • PK 값은 비지니스적으로 의미없는 Long값으로 설정하는것이 유연성에 좋다.

 

문제점 해결

  • 중간 테이블을 실제 Entity로 만들어서 관리하면 된다.
  • @ManyToMany
    • @OneToMany
    • @ManyToOne
@Entity
@Table(name = "tutor_language")
public class TutorLanguage {

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

    @ManyToOne
    @JoinColumn(name = "tutor_id")
    private Tutor tutor;

    @ManyToOne
    @JoinColumn(name = "language_id")
    private Language language;

    private Integer level;

    private String license;

}
@Entity
@Table(name = "language")
public class Language {

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

    private String name;

    @OneToMany(mappedBy = "language")
    private List<TutorLanguage> tutorLanguages = new ArrayList<>();

}
@Entity
@Table(name = "tutor")
public class Tutor {

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

    private String name;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Company company;

    @OneToOne
    @JoinColumn(name = "address_id", unique = true)
    private Address address;

    @OneToMany(mappedBy = "tutor")
    private List<TutorLanguage> tutorLanguages = new ArrayList<>();

    public Tutor() {
    }

    public Tutor(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
  • 실행결과
    • 기존 테이블 삭제 후 실행(충돌 방지)
    • <class>org.example.entity.TutorLanguage</class> **주석제거

'DB > JPA ( Java Persistence API )' 카테고리의 다른 글

[JPA] Proxy  (0) 2025.01.14
[JPA] 상속관계 매핑  (0) 2025.01.13
[JPA] Spring Data JPA  (0) 2025.01.07
[JPA] 연관관계 Mapping  (0) 2025.01.06
[JPA] Entity  (1) 2025.01.05

Formatter

📌 주로 사용자 지정 포맷을 적용해 데이터 변환을 처리할 때 사용된다. FormatterConversionService와 비슷한 목적을 가지지만 문자열을 객체로 변환하거나 객체를 문자열로 변환하는 과정에서 포맷팅을 세밀하게 제어할 수 있다.

  • 객체를 특정한 포맷에 맞춰서 문자로 출력하는 기능에 특화된것이 Fomatter이다.
  • Converter보다 조금 더 세부적인 기능이라고 생각하면 된다.
더보기

Spring에서 FormatterConverter는 모두 데이터 변환과 관련이 있지만, 주요 사용 목적기능에서 차이가 있습니다. 이 둘은 서로 보완적으로 사용되며, 특정 상황에서 더 적합한 도구를 선택할 수 있습니다.


Formatter와 Converter의 차이

특징 Formatter Converter
주요 목적 데이터 변환과 포맷팅(formatting) 지원. 단순히 타입 변환에 집중.
양방향 변환 String ↔ Object 형태로 양방향 변환 지원. 한 방향으로만(Source → Target) 변환 가능.
대상 데이터 주로 사용자 입력 데이터를 처리하거나 포맷팅이 필요한 데이터. 데이터 타입 간 변환(예: String → Integer, String → Enum).
인터페이스 구조 Formatter<T> (포맷 변환 및 역변환 메서드 포함). Converter<S, T> (단일 변환 메서드만 포함).
사용 위치 사용자 입력 데이터(폼 데이터, 요청 파라미터 등) 처리. 내부적으로 데이터 바인딩 및 단순 변환 처리.
Spring 사용 방식 FormattingConversionService를 통해 등록 및 관리. ConversionService를 통해 등록 및 관리.

Formatter

Formatter는 주로 데이터를 특정 형식(포맷)으로 변환하거나, 문자열 데이터를 객체로 역변환하는 데 사용됩니다.

인터페이스 정의

public interface Formatter<T> {
    String print(T object, Locale locale);   // 객체 → 문자열 (포맷팅)
    T parse(String text, Locale locale) throws ParseException; // 문자열 → 객체 (역변환)
}

Formatter 사용 예: String ↔ LocalDate

public class LocalDateFormatter implements Formatter<LocalDate> {

    @Override
    public LocalDate parse(String text, Locale locale) throws ParseException {
        return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }

    @Override
    public String print(LocalDate object, Locale locale) {
        return object.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }
}

Formatter 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new LocalDateFormatter());
    }
}

사용 예

컨트롤러 메서드에서 LocalDate를 자동 변환:

@GetMapping("/date")
public String getDate(@RequestParam LocalDate date) {
    return "Formatted Date: " + date.toString();
}

요청:

GET /date?date=2023-12-25

Converter

Converter는 단순히 하나의 데이터 타입을 다른 데이터 타입으로 변환합니다.

인터페이스 정의

public interface Converter<S, T> {
    T convert(S source); // 소스 타입 → 대상 타입
}

Converter 사용 예: String → Integer

public class StringToIntegerConverter implements Converter<String, Integer> {

    @Override
    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

Converter 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToIntegerConverter());
    }
}

사용 예

컨트롤러 메서드에서 Integer를 자동 변환:

@GetMapping("/number")
public String getNumber(@RequestParam Integer number) {
    return "Number: " + number;
}

요청:

GET /number?number=123

Formatter와 Converter의 사용 사례 비교

기능 Formatter Converter
날짜 변환 2023-12-25 ↔ LocalDate 지원하지 않음 (포맷팅 기능 없음).
Enum 변환 ACTIVE ↔ Status.ACTIVE String → Status.ACTIVE.
사용자 입력 처리 사용자 입력 데이터를 읽어 포맷팅. 단순 데이터 변환 작업.
예제 요청 파라미터 GET /date?date=2023-12-25 GET /number?number=123.

Converter와 Formatter의 동작 구조

  1. Converter:
    • 입력 데이터가 단순히 변환되기만 하면 되는 경우 사용.
    • : 쿼리 파라미터 123 → Integer.
  2. Formatter:
    • 사용자 입력 데이터가 특정 형식에 따라 변환되고 역변환이 필요한 경우 사용.
    • : yyyy-MM-dd → LocalDate.

결론

  • Converter: 단순히 데이터 타입을 변환할 때 사용.
    • 비유: 변환기. 한 단위를 다른 단위로 바꾸는 도구.
  • Formatter: 데이터 변환과 동시에 포맷팅이 필요한 경우 사용.
    • 비유: 포맷터. 데이터를 사람이 읽기 좋은 형식으로 변환하는 도구.

선택 기준:

  • 단순 타입 변환: Converter 사용.
  • 포맷팅과 역변환 모두 필요: Formatter 사용.
  • Locale
    • 지역 및 언어 정보를 나타내는 객체.
      • 언어코드 en, ko
      • 국가코드 US, KR
    • 특정 지역 및 언어에 대한 정보를 제공하여 국제화 및 지역화 기능을 지원한다.
    • 국제화
      • Locale 정보에 따라서 한글을 보여줄지 영문을 보여줄지 선택할 수 있다.

 

Formatter Interface

  • Printer, Parser 상속
  • 객체를 문자로 변환하고 문자를 객체로 변환하는 두가지 기능을 모두 가지고 있다.
  • Printer

Object를 String으로 변환한다.

 

Parser

String을 Object로 변환한다.

 

 

Formatter 적용

@Slf4j
public class PriceFormatter implements Formatter<Number> {
	
	@Override
  public Number parse(String text, Locale locale) throws ParseException {
    log.info("text = {}, locale={}", text, locale);
		
		// 변환 로직
		// NumberFormat이 제공하는 기능
		NumberFormat format = NumberFormat.getInstance(locale);
		// "10,000" -> 10000L
		return format.parse(text);
  }

  @Override
  public String print(Number object, Locale locale) {
			log.info("object = {}, locale = {}", object, locale);
			// 10000L -> "10,000"
      return NumberFormat.getInstance(locale).format(object);
  }
	
}

 

Number

  • Integer, Long, Double 등의 부모 클래스
class PriceFormatterTest {

		PriceFormatter formatter = new PriceFormatter();

    @Test
    void parse() throws ParseException {
        // given, when
        Number result = formatter.parse("1,000", Locale.KOREA);

        // then
        // parse 결과는 Long
        Assertions.assertThat(result).isEqualTo(1000L);
    }

    @Test
    void print() {
        // given, when
        String result = formatter.print(1000, Locale.KOREA);

        // then
        Assertions.assertThat(result).isEqualTo("1,000");
    }
}

 

 

 

Spring Formatter

📌 Spring의 Formatter는 문자열 데이터를 특정 객체로 변환하거나, 객체를 특정 문자열 형식으로 변환(포맷팅)하는 데 사용되는 인터페이스입니다. 데이터 포맷팅과 역변환을 쉽게 처리할 수 있도록 Spring에서 제공됩니다

 

 

FormattingConversionService

📌 ConversionServiceFormatter를 결합한 구현체로 타입 변환과 포맷팅이 필요한 모든 작업을 한 곳에서 수행할 수 있도록 설계되어 있어서 다양한 타입의 변환과 포맷팅을 쉽게 적용할 수 있다.

  • 어댑터 패턴을 사용하여 Formatter가 Converter처럼 동작하도록 만들어준다.

 

DefaultFormattingConversionService

📌 FormattingConversionService + 통화, 숫자관련 Formatter를 추가한것

public class FormattingConversionServiceTest {

    @Test
    void formattingConversionService() {

        // given
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

        // Converter 등록
        conversionService.addConverter(new StringToPersonConverter());
        conversionService.addConverter(new PersonToStringConverter());
        // Formatter 등록
        conversionService.addFormatter(new PriceFormatter());

        // when
        String result = conversionService.convert(10000, String.class);

        // then
        Assertions.assertThat(result).isEqualTo("10,000");

    }

}
  • ConversionService가 제공하는 convert()를 사용하면 된다.

 

SpringBoot의 기능

📌 SpringBoot는 기본적으로 WebConversionService를 사용한다.

  • DefaultFormattingConversionService 상속

 

 

Spring이 제공하는 Formatter

📌 Spring은 어노테이션 기반으로 원하는 형식Formatter를 사용할 수 있도록 기능을 제공한다.

  • Annotation
    • DTO 필드들에 적용 가능
    1. @NumberFormat
      • 숫자 관련 지정 Formatter 사용
      • NumberFormatAnnotationFormatterFactory
    2. @DateTimeFormat
      • 날짜 관련 지정 Formatter 사용
      • Jsr310DateTimeFormatAnnotationFormatterFactory
 

Spring Field Formatting :: Spring Framework

As discussed in the previous section, core.convert is a general-purpose type conversion system. It provides a unified ConversionService API as well as a strongly typed Converter SPI for implementing conversion logic from one type to another. A Spring conta

docs.spring.io

 

 

 

'Back-End (Web) > Spring' 카테고리의 다른 글

fetch join  (1) 2025.01.21
API 예외처리  (0) 2025.01.19
TypeConverter  (0) 2025.01.10
HttpMessageConverter  (0) 2025.01.09
ArgumentResolver  (0) 2025.01.08

TypeConverter

📌 Spring에서 객체의 타입을 서로 변환하는 데 사용되는 인터페이스로 Spring의 데이터 바인딩 과정에서 문자열을 특정 객체로 변환하거나 하나의 객체 타입을 다른 타입으로 변환할 때 사용한다.

  • 문자를 숫자로, 숫자를 문자로 변환하는 등 Web Application을 만들다보면 Type을 변환해야 하는 경우가 많이 발생한다.

 

  • 결론
    1. 요청 파라미터로 전달하는 10 값은 실제로는 문자열(String) 10이다.
    2. @RequestParam을 사용하면 문자 10을 Integer 타입의 숫자 10으로 변환된다.
    3. @ModelAttribute, @PathVariable 에서도 타입 변환을 확인할 수 있다.
    • Spring 내부에서 누군가가 타입을 자동으로 변환한다.

 

Converter Interface

  • Spring이 제공하는 인터페이스
    • implements하여 Converter로 등록하면 된다.
  • Converter는 모든 타입(T)에 적용할 수 있다.
  • 개발자가 새로운 Type을 만들어서 사용할 수 있도록 만든다.
    • 변환하고자 하는 타입에 맞춰서 Type Converter를 구현하고 등록하면 된다.

 

Converter

📌 데이터 타입 간 변환을 처리하는 인터페이스 , 주로 웹 요청 파라미터를 Java 객체로 변환하거나 그 반대로 변환할 때 사용되며 커스텀 변환 로직을 정의할 수 있다.

 

  • 주의점
    • org.springframework.core.convert.converter
    • Spring의 Converter와 같은 이름을 가진 Interface가 많으니 주의해야 한다.

 

 

  • 코드 예시
    • String → Integer
    • Converter<S, T> 에서 S는 변환할 Source T는 변환할 Type으로 설정하면 된다.
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
	
	@Override
	public Integer convert(String source) {
		log.info("source = {}", source);
		// 검증
		return Integer.valueOf(source);
	}
	
}
  • 파라미터로 들어온 source가 Interger로 변환된다.
  • Integer → String
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
	
	@Override
	public String convert(Integer source) {
		log.info("source = {}", source);
		return String.valueOf(source);
	}
}
  • 파라미터로 들어온 source가 String으로 변환된다.
  • String → Person
@Getter
public class Person {
	
		// 이름
		private String name;
		// 나이
		private int age;
	
		public Person(String name, int age) {
			this.name = name;
			this.age = age;
		}
	
}
  • 요청 예시
    • localhost:8080/type-converter?person=wonuk:1200
public class StringToPersonConverter implements Converter<String, Person> {
		// source = "wonuk:1200"
		@Override
		public Person convert(String source) {
			// ':' 를 구분자로 나누어 배열로 만든다.
			String[] parts = source.split(":");
	
			// 첫번째 배열은 이름이다. -> wonuk
	    String name = parts[0];
	    // 두번째 배열은 개월수이다. -> 1200
	    int months = Integer.parseInt(parts[1]);
	    
			// 개월수 나누기 12로 나이를 구하는 로직 (12개월 단위만 고려)
			int age = months / 12;
	
			return new Person(name, age);
		}
}
public class PersonToStringConverter implements Converter<Person, String> {
		
		@Override
		public String convert(Person source) {
				// 이름
				String name = source.getName();
				// 개월수
				int months = source.getAge * 12;
				// "wonuk:1200"
				return name + ":" + months;
		}
	
}

 

  • TypeConverter 사용
    • 구현은 단순하게 직접 메서드를 구현하여 모듈화 하면된다.
    • TypeConverter 를 생성하여 직접 사용하면 컨트롤러에서 변환하는 방식과 큰 차이가 없다.
PersonToStringConverter converter = new PersonToStringConverter();
String source = "wonuk:1200";
converter.convert(source);

 

  • Converter를 편리하게 등록하고 사용할 수 있도록 만들어주는 기능이 필요하다.
  • Spring은 String, Integer, Enum등 자주 사용되는 타입에 대한 컨버터를 제공하고 사용할 수 있도록 등록되어 있다.

 

 

Spring의 다양한 Converter

📌 Spring에서 제공하는 다양한 Converter 인터페이스가 존재하며 이들은 Spring의 데이터 바인딩, 요청/응답 처리, 속성 값 주입 등에 사용되고 ConversionService를 통해 등록 및 관리된다.

 

  1. Converter
    • 기본적인 변환을 담당하는 인터페이스
    • 단일 타입에서 단일 타입으로 변환할 때 사용한다.
      • Converter<Source, Type>
  2. ConverterFactory
    • 클래스 계층 구조가 복잡한 경우 사용
    • 기본 타입과 다양한 서브 타입 간의 변환을 지원한다.
  3. GenericConverter
    • 다양한 타입 간의 유연한 변환을 지원한다.
    • 복잡한 타입 변환 로직을 구현할 때 유리하다.
  4. ConditionalGenericConverter
    • GenericConverter 의 확장형으로 특정 조건에서만 타입 변환을 수행한다.
    • 추가적으로 matches() 를 통해 변환 가능 여부를 판단할 수 있다.

 

ConversionService

📌 Spring은 Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공한다. 이것이 Conversion Service 이다.

 

ConversionService 인터페이스

 

  1. canConvert()
    • Convert 가능 여부를 확인하는 기능
  2. convert()
    • 실제 변환하는 기능

 

 

DefaultConversionService

📌 Spring의 표준 ConversionService로 기본 제공 Converter와 확장 가능성을 통해 다양한 타입 변환을 유연하게 처리할 수 있도록 지원한다.

 

  • DefaultConversionService
    • ConversionService를 구현한 구현체

 

ConvertRegistry에 다양한 Converter를 등록한다.

 

 

 

  • ConverterRegistry
    • Converter를 등록하고 관리하는 기능을 제공한다.

import static org.assertj.core.api.Assertions.*;

public class ConversionServiceTest {

    @Test
    void defaultConversionService() {
        // given
        DefaultConversionService dcs = new DefaultConversionService();
        dcs.addConverter(new StringToPersonConverter());
        Person wonuk = new Person("wonuk", 100);
        
				// when
        Person stringToPerson = dcs.convert("wonuk:1200", Person.class);
        
				// then
        assertThat(stringToPerson.getName()).isEqualTo(wonuk.getName());
        assertThat(stringToPerson.getAge()).isEqualTo(wonuk.getAge());
    }

}
  • 컨버터를 사용할 때는 종류를 몰라도된다.
  • 컨버터는 ConversionService 내부에서 숨겨진채 제공된다.
    • 반환 타입, 파라미터 타입, 제네릭 등으로 ConversionService가 컨버터를 찾는다.
  • 즉, 클라이언트는 ConversionService 인터페이스만 의존하면 된다.
    • 컨버터 등록과 사용의 분리

 

ISP(인터페이스 분리 원칙, Interface Segregation Principal)

📌 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리하는 원칙

  • DefaultConversionService
    1. ConversionRegistry : 컨버터 등록
    2. ConversionService ****: ****컨버터 사용
  • 인터페이스를 분리하면 컨버터를 사용하는 클라이언트는 필요한 메서드만 알면된다.
  • ConversionRegistry 가 변경되어도 ConversionService와 연관이 없다.
  • Spring은 내부적으로 위와같이 등록, 사용이 분리된 인터페이스들이 아주 많다.
Spring은 내부적으로 ConversionService를 사용해 타입을 변환한다. 대표적으로 @RequestParam , @PathVariable, @ModelAttribute 등이 해당 기능을 사용한다.

 

Converter 요약

Spring의 **Converter**는 데이터 타입 간 변환을 처리하는 인터페이스입니다. 주로 Spring의 데이터 바인딩 또는 사용자 정의 변환 작업에 사용됩니다. 간단한 입력 값 변환에서부터 복잡한 객체 변환까지 유연하게 지원합니다.


Converter의 주요 특징

  1. 데이터 타입 변환:
    • 소스 타입(Source Type)에서 대상 타입(Target Type)으로 변환.
    • 예: String → Integer, String → LocalDate.
  2. 간결한 인터페이스:
    • 구현이 간단하며 특정 변환 작업에 집중.
  3. 범용 사용 가능:
    • Spring의 데이터 바인딩, 요청 파라미터 변환, 커스텀 변환 로직 등에 사용.
  4. 확장 가능:
    • Spring이 기본으로 제공하는 변환기 외에도 사용자 정의 변환기를 구현할 수 있음.

Converter 인터페이스

public interface Converter<S, T> {
    T convert(S source);
}
  • S: 소스 데이터 타입 (변환 전 데이터 타입).
  • T: 대상 데이터 타입 (변환 후 데이터 타입).

Converter의 사용 사례

1. 문자열을 날짜로 변환 (String → LocalDate)

public class StringToLocalDateConverter implements Converter<String, LocalDate> {

    @Override
    public LocalDate convert(String source) {
        // 문자열을 LocalDate로 변환
        return LocalDate.parse(source, DateTimeFormatter.ISO_DATE);
    }
}

Spring에서 Converter 등록

Spring에서는 변환기를 전역적으로 등록하거나, 특정 컨텍스트에서 사용할 수 있습니다.

1. 전역 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 커스텀 Converter 등록
        registry.addConverter(new StringToLocalDateConverter());
    }
}

Spring의 기본 Converter

Spring은 이미 다양한 기본 변환기를 제공합니다:

  • Primitive 타입 변환: String → int, String → double 등.
  • Java 날짜/시간 변환: String → LocalDate, String → LocalDateTime.
  • Enum 변환: String → Enum.

Converter와 일상적인 비유

비유: 데이터를 변환하는 "도구 상자"

  • Converter는 한 가지 타입의 데이터를 다른 타입으로 변환하는 간단한 도구입니다.
  • 예를 들어, 숫자 데이터를 텍스트로 변환하는 계산기처럼, Converter는 입력 값을 원하는 형태로 만들어줍니다.

Converter 사용 예제

컨트롤러에서 사용

@RestController
@RequestMapping("/api")
public class ExampleController {

    @GetMapping("/date")
    public String getDate(@RequestParam String date, Converter<String, LocalDate> converter) {
        LocalDate localDate = converter.convert(date);
        return "Converted date: " + localDate.toString();
    }
}

요청 예

GET /api/date?date=2023-12-25

결과

Converted date: 2023-12-25

Converter와 관련된 확장

  1. Formatter:
    • Converter의 확장형으로, 데이터를 특정 형식(포맷)으로 변환하고 역변환도 지원.
    • 예: 날짜를 특정 형식으로 변환하거나 파싱.
  2. GenericConverter:
    • 보다 범용적인 변환을 지원하는 고급 형태의 Converter.
  3. ConversionService:
    • 여러 변환기를 한데 모아 관리하고, 필요한 변환기를 자동으로 찾아주는 Spring의 변환 관리 서비스.

Converter와 HttpMessageConverter의 차이

  • Converter:
    • 데이터 타입 간 변환.
    • String → Integer, String → LocalDate 등 간단한 데이터 변환에 초점.
  • HttpMessageConverter:
    • HTTP 요청/응답 데이터를 변환.
    • JSON → Java 객체, Java 객체 → JSON 등, 네트워크 통신에 초점.

결론

Spring의 Converter는 데이터 타입을 변환하는 단순하고 강력한 도구입니다. 데이터를 변환하는 간단한 작업부터, 프로젝트 전반에서 데이터 바인딩에 활용됩니다.

비유: 필요한 데이터를 원하는 형태로 "가공"하는 변환기입니다.

 

 

'Back-End (Web) > Spring' 카테고리의 다른 글

API 예외처리  (0) 2025.01.19
Formatter  (0) 2025.01.11
HttpMessageConverter  (0) 2025.01.09
ArgumentResolver  (0) 2025.01.08
[Spring] 스프링 정리  (0) 2025.01.07

HttpMessageConverter

📌 클라이언트와 서버 간의 HTTP 요청과 응답을 처리할 때 데이터 형식 변환을 담당 한다. 클라이언트가 보낸 데이터를 서버가 이해할 수 있는 형태로 변환하거나, 서버가 응답으로 보내는 데이터를 클라이언트가 이해할 수 있는 형태로 변환할 때 사용됩니다. [ View를 응답하는 것이 아닌, Rest API(HTTP API)로 JSON, TEXT, XML 등의 데이터를 응답 Message Body에 직접 입력하는 경우 HttpMessageConverter를 사용한다. ]

  • 1. SSR → @Controller + View Template → 서버 측에서 화면을 동적으로 그린다.
  • 2. CSR → @RestController + Data → 클라이언트 측에서 화면을 동적으로 그린다.
  • 3. 실제로는 두가지 기술이 함께 사용되는 경우가 많다.

 

  • HTTP 응답 메세지 Body에 데이터를 직접 입력 후 반환한다.
  • 요청 Accept Header + Controller 반환 타입

ViewResolver가 아닌 HttpMessageConverter가 동작한다.

 

  • HttpMessageConverter가 적용되는 경우
    1. HTTP 요청 : @RequestBody, HttpEntity<>, RequestEntity<>
    2. HTTP 응답 : @ResponseBody, HttpEntity<>, ResponseEntity<>
    • HttpMessageConverter는 요청과 응답 모두 사용된다.
@RestController = @Controller + @ResponseBody

 

 

우선순위

📌 Spring은 다양한 HttpMessageConverter를 제공하고 있고 우선순위가 있다. 대상 Class와 MediaType을 체크해서 어떤 Converter를 사용할지 결정한다.

 

 

동작 순서와 예시

 

요청 데이터 읽기

더보기

위 코드는 Spring Framework에서 REST API를 구현한 예제입니다. RESTful 엔드포인트를 제공하며, 클라이언트로부터 JSON 데이터를 받고, 처리 후 JSON 데이터를 응답으로 반환합니다. 아래에서 각 부분을 상세히 분석하겠습니다.


1. 클래스 수준 어노테이션

@RestController
  • @RestController:
    • **@Controller**와 **@ResponseBody**를 합친 기능을 합니다.
    • 이 클래스의 모든 메서드는 기본적으로 JSON 또는 XML 형식으로 응답을 생성합니다. (HTML View가 아닌 데이터만 반환)
    • 따라서 각 메서드에 **@ResponseBody**를 명시할 필요가 없습니다.

2. 메서드 수준 어노테이션

@PostMapping(value = "/example", produces = "application/json")
  • @PostMapping:
    • HTTP POST 요청을 처리하는 메서드를 정의합니다.
    • value = "/example": 이 API는 /example URL에 매핑됩니다.
    • produces = "application/json": 이 API는 클라이언트에게 JSON 형식으로 응답을 반환합니다.

3. 메서드 파라미터

public ResponseDto example(@RequestBody RequestDto dto)
  • @RequestBody:
    • 클라이언트가 요청 본문에 포함한 데이터를 읽어와, Java 객체(RequestDto)로 변환합니다.
    • Spring은 **HttpMessageConverter**를 사용해 JSON 데이터를 RequestDto로 변환합니다.
    • 이 메서드는 클라이언트가 보낸 JSON 데이터를 받아, 내부에서 사용할 수 있는 DTO 객체로 변환하여 사용합니다.
    • 예: 클라이언트가 다음과 같은 JSON 데이터를 보냈다고 가정:
      {
        "id": 123,
        "name": "Example"
      }
      
      -> Spring이 이 JSON 데이터를 RequestDto 객체로 변환합니다.

4. 메서드 로직

ResponseDto responseDto = service.example(dto);
  • service.example(dto):
    • 서비스 계층으로 RequestDto 데이터를 전달하여, 요청을 처리합니다.
    • 서비스 계층은 보통 비즈니스 로직을 처리하는 곳입니다. (데이터베이스 호출, 데이터 변환 등)
    • 처리 결과를 ResponseDto 객체로 반환받습니다.
return responseDto;
  • 반환값:
    • ResponseDto 객체를 반환합니다.
    • Spring은 **HttpMessageConverter**를 사용해 ResponseDto 객체를 JSON으로 변환하여 클라이언트에게 응답합니다.

5. DTO(Data Transfer Object)

  • RequestDto:
    • 클라이언트에서 전송한 데이터를 담는 객체입니다.
    • 예시:
      public class RequestDto {
          private int id;
          private String name;
      
          // Getters and Setters
      }
      
  • ResponseDto:
    • 처리 결과를 클라이언트로 반환할 때 사용하는 객체입니다.
    • 예시:
      public class ResponseDto {
          private String status;
          private String message;
      
          // Getters and Setters
      }
      

6. 전체 실행 흐름

  1. 클라이언트가 /example로 POST 요청을 보냅니다. 요청 본문에 JSON 데이터를 포함합니다.
    {
        "id": 123,
        "name": "Example"
    }
    
  2. Spring은 JSON 데이터를 RequestDto 객체로 변환합니다.
  3. example 메서드가 호출되어 RequestDto 데이터를 서비스 계층으로 전달합니다.
  4. 서비스 계층이 요청 데이터를 처리하고, 결과를 ResponseDto로 반환합니다.
  5. ResponseDto 객체는 다시 JSON으로 변환되어 클라이언트에게 응답됩니다.
    {
        "status": "success",
        "message": "Operation completed"
    }
    

7. 주요 특징

  • RESTful API: 데이터만을 전송하고 반환하는 RESTful 스타일의 API입니다.
  • JSON 데이터 처리: HttpMessageConverter를 통해 JSON 데이터를 주고받습니다.
  • 계층화: 서비스 계층을 사용해 비즈니스 로직과 컨트롤러 로직을 분리합니다.
  • 재사용성: DTO 객체를 통해 데이터를 깔끔하게 전달하고 관리합니다.
더보기

boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType)는 Spring Framework의 HttpMessageConverter 인터페이스에서 정의된 메서드 중 하나로, 특정 타입의 객체를 지정된 미디어 타입(MediaType)으로 변환할 수 있는지 확인합니다. 이 메서드는 HTTP 응답을 생성할 때 사용됩니다.


메서드 정의

boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

파라미터

  1. Class<?> clazz:
    • HTTP 응답으로 변환하려는 Java 객체의 클래스 타입입니다.
    • 예: ResponseDto.class, String.class 등.
    • 이 매개변수를 통해 현재 HttpMessageConverter가 처리 가능한 객체인지 확인합니다.
  2. @Nullable MediaType mediaType:
    • 변환 대상의 **미디어 타입(MediaType)**입니다.
      예: application/json, application/xml, text/plain 등.
    • @Nullable이므로 null일 수도 있습니다.
      • null인 경우 미디어 타입과 관계없이 클래스 타입만 확인합니다.

반환값

  • true: 이 HttpMessageConverter가 지정된 클래스와 미디어 타입에 대해 쓰기 작업(HTTP 응답 데이터 변환)을 처리할 수 있습니다.
  • false: 처리할 수 없으면 false를 반환합니다.

메서드 동작

이 메서드는 Spring이 적절한 **HttpMessageConverter**를 선택할 때 사용됩니다. 요청 데이터의 Java 객체 타입과 응답 데이터의 미디어 타입을 비교하여 적합한 컨버터를 결정합니다.

  1. Spring은 등록된 여러 HttpMessageConverter를 순차적으로 탐색합니다.
  2. 각 컨버터의 canWrite 메서드를 호출하여, 특정 객체와 미디어 타입을 처리할 수 있는지 확인합니다.
  3. 적합한 컨버터를 찾으면 해당 컨버터가 쓰기 작업을 수행합니다.

사용 예시

JSON 변환 컨버터 예

MappingJackson2HttpMessageConverter는 JSON 데이터를 처리하는 HttpMessageConverter 구현체입니다. 이를 기준으로 canWrite 메서드를 구현하면 다음과 같이 동작합니다:

@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
    // JSON 컨버터는 기본적으로 모든 객체 타입을 지원
    if (mediaType == null || mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
        return true; // JSON 형식으로 변환 가능
    }
    return false; // JSON 외 미디어 타입은 변환 불가
}

동작 방식

  1. clazz: 응답으로 반환할 객체의 클래스가 전달됩니다. 예: ResponseDto.class.
  2. mediaType: 응답 헤더의 Content-Type에 설정된 미디어 타입이 전달됩니다.
    • 예: application/json, application/xml, text/plain.

만약 다음 요청이 왔다고 가정:

GET /api/example HTTP/1.1
Accept: application/json

Spring은 다음을 수행합니다:

  • MappingJackson2HttpMessageConverter.canWrite(ResponseDto.class, application/json)를 호출합니다.
  • canWrite가 true를 반환하면, JSON으로 응답을 변환합니다.

유스 케이스

  1. 클라이언트 요청 처리:
    • 클라이언트가 Accept 헤더에 특정 미디어 타입을 요청한 경우, 해당 미디어 타입으로 응답을 생성할 수 있는지 확인합니다.
  2. 다양한 컨버터 사용:
    • Spring은 여러 컨버터를 지원하므로, 각 컨버터가 자신이 지원하는 클래스와 미디어 타입만 처리하도록 canWrite를 구현합니다.
      • 예: MappingJackson2HttpMessageConverter는 JSON 처리.
      • StringHttpMessageConverter는 단순 텍스트 처리.

흐름 예시

다음은 Spring이 canWrite를 호출하는 시나리오입니다:

  1. 컨트롤러에서 객체를 반환:
  2. @GetMapping("/example") public ResponseDto example() { return new ResponseDto("Success", "Operation completed"); }
  3. Spring이 등록된 HttpMessageConverter들을 순회하며 canWrite를 호출:
    • MappingJackson2HttpMessageConverter.canWrite(ResponseDto.class, application/json)
    • 반환값: true
  4. MappingJackson2HttpMessageConverter를 사용해 ResponseDto를 JSON으로 변환:
  5. { "status": "Success", "message": "Operation completed" }

정리

  • **canWrite**는 특정 객체(clazz)를 특정 미디어 타입(mediaType)으로 변환 가능한지 확인합니다.
  • Spring은 이 메서드를 사용해 적합한 HttpMessageConverter를 결정합니다.
  • REST API 응답에서 주로 활용되며, JSON, XML, TEXT 등의 데이터 형식을 처리할 수 있는지 판단하는 핵심 메서드입니다.
더보기

void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)는 Spring의 HttpMessageConverter 인터페이스에서 정의된 메서드로, 데이터를 HTTP 응답 메시지에 쓰는 역할을 합니다. 이 메서드는 REST API에서 서버가 클라이언트로 데이터를 반환할 때 사용됩니다.


메서드 정의

void write(
    T t,
    @Nullable MediaType contentType,
    HttpOutputMessage outputMessage
) throws IOException, HttpMessageNotWritableException;

파라미터 설명

  1. T t:
    • HTTP 응답 본문에 쓰려는 데이터 객체입니다.
    • 예를 들어, 컨트롤러에서 반환하는 DTO 객체가 여기에 전달됩니다.
    • 데이터의 타입은 HttpMessageConverter의 제네릭 타입 T로 제한됩니다.
  2. @Nullable MediaType contentType:
    • 응답의 Content-Type을 나타냅니다.
    • 예: application/json, application/xml, text/plain.
    • @Nullable이므로 null일 수 있습니다.
      • null인 경우, 기본값이나 자동 결정된 MediaType이 사용됩니다.
  3. HttpOutputMessage outputMessage:
    • HTTP 응답 메시지를 나타내는 객체입니다.
    • 실제로 응답 데이터를 쓰는 데 필요한 출력 스트림(OutputStream)과 헤더(HttpHeaders)를 포함합니다.

예외

  1. IOException:
    • 데이터 쓰기 중 IO 문제가 발생할 경우 던집니다.
    • 예: 네트워크 연결 문제.
  2. HttpMessageNotWritableException:
    • 데이터 객체를 변환하거나 쓰는 데 실패한 경우 던집니다.
    • 예: 객체를 JSON으로 변환하지 못하거나 변환 로직에 오류가 있는 경우.

메서드의 역할

이 메서드는 데이터(t)를 지정된 형식(contentType)으로 변환하고, 이를 HTTP 응답 메시지(outputMessage)의 본문에 쓰는 작업을 수행합니다.


실제 동작 흐름

  1. 컨트롤러에서 데이터 반환:
    • 예: 컨트롤러 메서드가 DTO 객체를 반환.
    @GetMapping("/example")
    public ResponseDto example() {
        return new ResponseDto("success", "Operation completed");
    }
    
  2. Spring이 적절한 HttpMessageConverter 선택:
    • canWrite 메서드를 사용해 변환 가능한 컨버터를 선택합니다.
    • 예: MappingJackson2HttpMessageConverter가 JSON 변환을 담당.
  3. write 메서드 호출:
    • Spring이 선택한 컨버터의 write 메서드를 호출해, 반환 객체(ResponseDto)를 JSON으로 변환하고 HTTP 응답 본문에 씁니다.

구현 예: JSON 변환기

MappingJackson2HttpMessageConverter의 write 메서드가 어떻게 동작할지 간단히 구현 예로 살펴보겠습니다.

@Override
public void write(
    Object t,
    @Nullable MediaType contentType,
    HttpOutputMessage outputMessage
) throws IOException {
    // 1. JSON 변환기 초기화 (예: ObjectMapper 사용)
    ObjectMapper objectMapper = new ObjectMapper();

    // 2. Content-Type 설정
    if (contentType != null) {
        outputMessage.getHeaders().setContentType(contentType);
    } else {
        outputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    }

    // 3. 객체를 JSON으로 변환하여 OutputStream에 쓰기
    OutputStream bodyStream = outputMessage.getBody();
    objectMapper.writeValue(bodyStream, t);

    // 4. OutputStream 닫기 (Spring이 자동으로 관리)
}

사용 예제

클라이언트 요청

GET /example HTTP/1.1
Accept: application/json

컨트롤러 반환

@GetMapping("/example")
public ResponseDto example() {
    return new ResponseDto("success", "Operation completed");
}

write 메서드 동작

  1. 파라미터 전달:
    • t: ResponseDto 객체 ({"status":"success", "message":"Operation completed"}).
    • contentType: application/json.
    • outputMessage: 클라이언트로 보낼 HTTP 응답 메시지.
  2. 동작:
    • ObjectMapper가 ResponseDto 객체를 JSON 문자열로 변환.
    • 변환된 JSON 문자열을 outputMessage.getBody()의 출력 스트림에 씀.
    • outputMessage.getHeaders()를 사용해 Content-Type 헤더를 설정.

결과 응답

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 58

{"status":"success","message":"Operation completed"}

주요 특징

  1. 데이터 변환:
    • write 메서드는 Java 객체를 HTTP 응답 형식(JSON, XML 등)으로 변환합니다.
    • 변환은 HttpMessageConverter 구현체에 따라 다릅니다.
  2. 스트림 기반 쓰기:
    • 변환된 데이터를 직접 HttpOutputMessage의 출력 스트림에 씁니다.
    • 이를 통해 대규모 데이터 처리도 효율적으로 수행할 수 있습니다.
  3. 유연성:
    • 미디어 타입이 null인 경우 기본값을 사용할 수 있습니다.
    • 개발자가 원하는 방식으로 데이터 변환을 커스터마이징할 수 있습니다.

정리

  • **write**는 데이터를 HTTP 응답 본문에 쓰는 메서드로, REST API에서 클라이언트에 데이터를 반환할 때 사용됩니다.
  • Spring의 HttpMessageConverter 구현체마다 이 메서드가 다르게 동작하여 다양한 데이터 형식(JSON, XML, TEXT 등)을 처리합니다.
  • 사용자는 커스터마이징된 HttpMessageConverter를 구현해 특정 데이터 포맷을 지원할 수도 있습니다.

 

응답 데이터 쓰기

 

HttpMessageConverter 구조

📌 HttpMessageConverter를 주로 사용하는 어노테이션 @RequestBody, @ResponseBody

  • 요청시에는 Argument Resolver가 사용하는것이다.
  • 응답시에는 ReturnValueHandler가 사용한다.

 

대표적인 ArgumentResolver, ReturnValueHandler

1. ArgumentResolver

**ArgumentResolver**는 컨트롤러 메서드 파라미터에 요청 데이터를 바인딩하는 역할을 합니다.

대표적인 ArgumentResolver

Resolver 이름 설명

RequestParamMethodArgumentResolver @RequestParam 어노테이션 처리. 요청의 쿼리 파라미터 또는 폼 데이터를 메서드 파라미터로 바인딩.
PathVariableMethodArgumentResolver @PathVariable 어노테이션 처리. URL 경로 변수 값을 메서드 파라미터로 바인딩.
RequestHeaderMethodArgumentResolver @RequestHeader 어노테이션 처리. HTTP 요청 헤더 값을 메서드 파라미터로 바인딩.
CookieValueMethodArgumentResolver @CookieValue 어노테이션 처리. HTTP 쿠키 값을 메서드 파라미터로 바인딩.
RequestBodyArgumentResolver @RequestBody 어노테이션 처리. 요청 본문(JSON/XML)을 Java 객체로 변환.
ModelAttributeMethodProcessor @ModelAttribute 어노테이션 처리. 요청 데이터를 객체에 바인딩하고, 모델에 추가.
SessionAttributeMethodArgumentResolver @SessionAttribute 어노테이션 처리. 세션에서 특정 속성을 가져와 메서드 파라미터에 주입.
RequestAttributeMethodArgumentResolver @RequestAttribute 어노테이션 처리. 요청 속성(request scope)을 메서드 파라미터에 바인딩.
HttpEntityMethodProcessor HttpEntity<T> 및 RequestEntity<T> 처리. 요청 전체(헤더와 본문 포함)를 객체로 바인딩.
PrincipalMethodArgumentResolver 현재 인증된 사용자의 정보를 나타내는 java.security.Principal 객체 처리.
DefaultMethodArgumentResolver HTTP 요청 및 응답 객체(HttpServletRequest, HttpServletResponse) 처리.

 


ArgumentResolver의 예제

@RequestParam과 @RequestBody 사용

@GetMapping("/greet")
public String greetUser(@RequestParam String name) {
    return "Hello, " + name;
}

@PostMapping("/process")
public String processData(@RequestBody MyDto data) {
    return "Processed: " + data.getValue();
}
  • RequestParamMethodArgumentResolver: name 값을 쿼리 파라미터에서 추출.
  • RequestBodyArgumentResolver: 요청 본문(JSON)을 MyDto 객체로 변환.

2. ReturnValueHandler

**ReturnValueHandler**는 컨트롤러 메서드가 반환한 데이터를 클라이언트에게 적절히 변환하여 응답으로 전달합니다.

대표적인 ReturnValueHandler

Handler 이름 설명

RequestResponseBodyMethodProcessor @ResponseBody와 HttpEntity를 처리. Java 객체를 JSON/XML 등으로 변환.
ModelAndViewMethodReturnValueHandler ModelAndView 객체를 처리하여 뷰를 렌더링.
ViewMethodReturnValueHandler View 객체를 처리하여 뷰를 렌더링.
HttpEntityMethodProcessor HttpEntity<T> 및 ResponseEntity<T>를 처리. 헤더와 본문을 설정하여 응답.
ModelMethodProcessor 모델 객체를 처리하여 View에서 사용할 데이터로 추가.
DeferredResultMethodReturnValueHandler 비동기 작업 결과를 처리(DeferredResult, CompletableFuture 등).
CallableMethodReturnValueHandler Callable 객체를 처리하여 비동기 응답 생성.
StringMethodReturnValueHandler 단순 문자열(String)을 처리하여 응답 본문에 쓰거나, 뷰 이름으로 해석.


ReturnValueHandler의 예제

@ResponseBody와 ResponseEntity 사용

@RestController
public class ExampleController {

    @GetMapping("/json")
    public MyDto getJson() {
        return new MyDto("data", 123); // RequestResponseBodyMethodProcessor 처리
    }

    @GetMapping("/response")
    public ResponseEntity<String> getResponse() {
        return ResponseEntity.ok("OK"); // HttpEntityMethodProcessor 처리
    }
}
  • RequestResponseBodyMethodProcessor:
    • MyDto 객체를 JSON 형식으로 변환하여 응답 본문에 작성.
  • HttpEntityMethodProcessor:
    • ResponseEntity의 상태 코드, 헤더, 본문을 설정하여 응답.

전체 흐름 정리

Spring MVC의 요청 처리 과정에서 **ArgumentResolver**와 **ReturnValueHandler**는 다음처럼 동작합니다:

  1. ArgumentResolver:
    • 클라이언트 요청 데이터를 컨트롤러 메서드 파라미터에 맞게 변환.
    • 예: 쿼리 파라미터 → @RequestParam, 본문(JSON) → @RequestBody.
  2. ReturnValueHandler:
    • 컨트롤러 메서드의 반환값을 클라이언트 응답으로 변환.
    • 예: 객체 → JSON, 문자열 → 뷰 이름 해석.

예제 통합

@RestController
public class ExampleController {

    @PostMapping("/submit")
    public ResponseEntity<String> submit(@RequestBody MyDto dto) {
        // ArgumentResolver: RequestBodyArgumentResolver 처리
        System.out.println("Received: " + dto.getValue());
        return ResponseEntity.ok("Processed successfully"); // ReturnValueHandler: HttpEntityMethodProcessor 처리
    }
}

요청:

POST /submit
Content-Type: application/json

{
    "value": "example"
}

응답:

HTTP/1.1 200 OK
Content-Type: text/plain

Processed successfully

이렇게 Spring MVC는 ArgumentResolver로 요청 데이터를 바인딩하고, ReturnValueHandler로 응답 데이터를 변환합니다!

 

요청과 응답

📌 ArgumentResolverHttpMessageConverter는 다르다.

 

 

 

WebMvcConfigurer

📌 Spring MVC의 설정을 사용자 정의할 수 있도록 제공되는 인터페이스로 implements하여 설정을 확장하거나 커스터마이징할 수 있다.

 

  • 주요 인터페이스
    1. HandlerMethodArgumentResolver
    2. HandlerMethodReturnValueHandler
    3. HttpMessageConverter
    • 모두 인터페이스로 구현되어 있으며 대부분 구현되어 있다.
      • Spring에서 기본적으로 제공하고 있다.
    • 개발자는 잘 사용하면 된다.

 

  • 기능의 확장
    • WebMvcConfigurer를 상속받고 Spring Bean으로 등록
public interface WebMvcConfigurer {
    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }

    default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    }

    default void addFormatters(FormatterRegistry registry) {
    }

    default void addInterceptors(InterceptorRegistry registry) {
    }

    default void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

    default void addCorsMappings(CorsRegistry registry) {
    }

    default void addViewControllers(ViewControllerRegistry registry) {
    }

    default void configureViewResolvers(ViewResolverRegistry registry) {
    }

    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    }

    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    }

    default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

    default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    @Nullable
    default Validator getValidator() {
        return null;
    }

    @Nullable
    default MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}
  1. addArgumentResolvers()
  2. addReturnValueHandlers()
  3. extendMessageConverters()
  • 필요한 메서드를 오버라이딩 하면된다.
  • @Configuration
    • @Component 를 포함하고 있다. (Spring Bean 등록이 된다.)

 

 

HttpMessageConverter 요약

HttpMessageConverter는 Spring MVC에서 클라이언트와 서버 간 데이터를 변환하는 데 사용되는 컴포넌트입니다. HTTP 요청과 응답의 본문을 Java 객체와 JSON, XML, TEXT 등으로 상호 변환합니다.


HttpMessageConverter의 역할

  1. 요청 처리:
    • 클라이언트가 JSON, XML 등 형식으로 요청 본문을 보내면 이를 Java 객체로 변환.
  2. 응답 생성:
    • 컨트롤러가 반환한 Java 객체를 JSON, XML, TEXT 등으로 변환하여 클라이언트로 전송.

일상적인 비유

  • 비유: 번역가(Translator)
    • 한 사람이 영어로 말을 하고, 다른 사람이 한국어로 이해하려면 번역가가 필요합니다.
    • 여기서 영어는 JSON, XML 등의 데이터 형식이고, 한국어는 Java 객체입니다.
    • 번역가(HttpMessageConverter)가 데이터를 양쪽에서 서로 이해할 수 있도록 변환합니다.

HttpMessageConverter의 동작 흐름

  1. 클라이언트 요청:
    • 클라이언트가 JSON 요청을 보냅니다.
    • 예: {"name": "John", "age": 30}
  2. 요청 데이터 변환:
    • HttpMessageConverter가 JSON 데이터를 Java 객체로 변환.
  3. 컨트롤러 처리:
    • 컨트롤러 메서드에서 비즈니스 로직 처리.
  4. 응답 데이터 변환:
    • 컨트롤러 메서드가 반환한 Java 객체를 JSON 형식으로 변환하여 클라이언트에 응답.

대표적인 HttpMessageConverter 구현체

Converter 이름 역할

MappingJackson2HttpMessageConverter JSON 데이터를 Java 객체로 변환 또는 반대로 변환.
MappingJackson2XmlHttpMessageConverter XML 데이터를 Java 객체로 변환 또는 반대로 변환.
StringHttpMessageConverter 문자열 데이터를 처리.
FormHttpMessageConverter application/x-www-form-urlencoded 폼 데이터를 처리.
ByteArrayHttpMessageConverter 바이너리 데이터를 처리 (예: 파일 다운로드).

HttpMessageConverter 사용 예제

요청 본문(JSON) → Java 객체 (@RequestBody)

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public String createUser(@RequestBody UserDto userDto) {
        return "User created: " + userDto.getName();
    }
}
  1. 클라이언트 요청:
  2. POST /api/users Content-Type: application/json { "name": "John", "age": 30 }
  3. HttpMessageConverter 동작:
    • MappingJackson2HttpMessageConverter가 JSON 데이터를 UserDto 객체로 변환.
    • 컨트롤러는 변환된 UserDto 객체를 사용.
  4. 컨트롤러 결과:
    • User created: John

Java 객체 → JSON 응답 (@ResponseBody)

@RestController
public class ExampleController {

    @GetMapping("/user/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return new UserDto(id, "John", 30); // Java 객체 반환
    }
}
  1. 클라이언트 요청:
  2. GET /user/1 Accept: application/json
  3. HttpMessageConverter 동작:
    • 컨트롤러에서 반환한 UserDto 객체를 MappingJackson2HttpMessageConverter가 JSON으로 변환.
  4. 클라이언트 응답:
  5. HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "name": "John", "age": 30 }

HttpMessageConverter의 장점

  1. 자동 변환:
    • JSON, XML, TEXT 등 다양한 데이터 형식을 Java 객체로 쉽게 변환 가능.
    • 개발자가 수동으로 데이터 변환 코드를 작성할 필요 없음.
  2. 확장 가능:
    • 필요에 따라 커스텀 HttpMessageConverter를 작성해 특별한 데이터 형식을 처리 가능.
  3. REST API 친화적:
    • RESTful 서비스를 개발할 때 요청과 응답 데이터를 간단하게 처리.

커스텀 HttpMessageConverter

Spring MVC에서 특정 데이터 포맷을 처리하기 위해 커스텀 HttpMessageConverter를 구현할 수 있습니다.

예제: CSV 데이터를 처리하는 HttpMessageConverter

  1. 커스텀 HttpMessageConverter 구현:
  2. public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<MyCsvDto> { public CsvHttpMessageConverter() { super(new MediaType("text", "csv")); } @Override protected boolean supports(Class<?> clazz) { return MyCsvDto.class.isAssignableFrom(clazz); } @Override protected MyCsvDto readInternal(Class<? extends MyCsvDto> clazz, HttpInputMessage inputMessage) throws IOException { // CSV 데이터를 Java 객체로 변환하는 로직 return new MyCsvDto("example", 123); } @Override protected void writeInternal(MyCsvDto myCsvDto, HttpOutputMessage outputMessage) throws IOException { // Java 객체를 CSV 데이터로 변환하는 로직 String csv = myCsvDto.getName() + "," + myCsvDto.getValue(); outputMessage.getBody().write(csv.getBytes()); } }
  3. Spring에 등록:
  4. @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new CsvHttpMessageConverter()); } }

결론

HttpMessageConverter는 클라이언트와 서버 간 데이터 변환을 자동화하여 개발자의 작업을 간소화합니다. 데이터를 JSON, XML 등 다양한 포맷으로 처리할 수 있으며, 필요에 따라 커스터마이징하여 확장할 수도 있습니다.

비유: 데이터를 주고받는 번역가로, 클라이언트와 서버가 서로 다른 언어(JSON, XML, Java 객체 등)를 이해할 수 있게 해주는 역할을 합니다.

'Back-End (Web) > Spring' 카테고리의 다른 글

Formatter  (0) 2025.01.11
TypeConverter  (0) 2025.01.10
ArgumentResolver  (0) 2025.01.08
[Spring] 스프링 정리  (0) 2025.01.07
[Spring] 의존관계 주입  (0) 2024.12.26

[문제 발생 + 문제 유추]
CartService를 만들다가 에러가 나옴, 처음에는 인식의 문제인줄 알아서 엔티티를 수정하려했다.

 

[원인 규명]

'setTotalPrice(java. lang. Integer)'이(가) 'com. threemeals. delivery. domain. cart. entity. Cart'에서 public이 아닙니다. 외부 패키지에서 액세스할 수 없습니다.

 

setTotalPrice 메서드의 접근 제한자가 public이 아니어서 발생한 것, @Setter를 적용했을 때, 접근 제한자는 기본적으로 필드의 접근 제한자를 따르게 된다. 따라서 @Setter가 적용된 필드가 private이거나 protected라면, setter 메서드도 private 또는 protected로 생성된다.

[해결]

storeRepository에 접근하여 직접 데이터를 불러오는 방식을 채택

 

[해결법 채택 이유]
보안상의 이유로, 아무나 접근할 수 있는 public으로 바꿀 수는 없으니

 


레디스 데이터 베이스 오류

 

[문제 발생 + 문제 유추]

코드 실행중에 db 관련 오류가 발생하였다. 확인하기 전에는 아마 간단한 설정 문제라고 생각을 하여 의존성을 확인했는데

 

[원인 규명]

그냥 데이터 베이스 설정을 못찾아서라는 간단한 오류였다. 다만 여기서 좀 헤메었던게, 마리아 db와 redis는 서로 같이 있어도 설정이 다르기 때문에 yml 파일에서 따로 설정해야했다. 

  datasource:
    url: jdbc:mariadb://localhost:3306/3meals?serverTimezone=UTC
    username: root
    password: 123!
  sql:
    init:
      mode: always

  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MariaDBDialect
    #        default_batch_fetch_size: 100
    hibernate:
      ddl-auto: create
    defer-datasource-initialization: true
  #    open-in-view: false

  redis:
    host: localhost
    port: 6379

 

[해결]

처음에는 redis 오류인줄 알았으나, 그냥 단순히 마리아 db의 설정을 하지 않아서 생긴 문제였다... 해결 이유는 따로 없고 그냥 설정을 안한거라 별 기술할 내용은 없지만, redis와 다른 DB는 완전히 따로 돌아가고 의존성 Config 같은 설정도 완전이 따로 해줘야한다.


[문제 발생 + 문제 유추]
JPA로 자동으로 테이블을 생성하게 해 두었는데, order 테이블이 생성이 안됨, JPA 자동 테이블 생성에서 등록을 안했는지 의심

 

[원인 규명]

의심과 다르게 자동 테이블에 정상적으로 등록이 되어있어서, 구글링해본 결과 mysql에서 order를 이번 약관부터 사용하다보니 order 테이블을 생성하지 못했던 거였음

[해결]

단순히 테이블 명을 orders로 수정하였음, 덕뿐에 일관성을 위해 다른 테이블도 다 s를 붙이는 형태로 수정함


[문제 발생 + 문제 유추]
코드 실행시 데이터 베이스에 id 값이 없다는 에러가 발생, @id로 자동으로 id개 배분되고 있었다보니 영문을 모르고 있었다. 그래서 어노테이션 쪽을 수정하고 있었는데.

 

[원인 규명]

의심과는 다르게, 레디스를 활용한 레포지토리 파일은 jpa의 영역 밖에 있다보니 @id를 사용한다고 id가 자동으로 배분이 될리가 없었다. 거기에 RDB와 다르게 레디스는 KEY-VALUE라 어노테이션이 필요없는 일반 자바 객체로 관리하는 방식어었다... 


[해결]

카트 ID를 USERID로 생성하도록 했다. 카트는 한명당 1개씩 간헐적으로 배분되게 설정했기 때문에 이렇게 진행해도 문제가 없었고 잠깐 DB에 저장될 장바구니 데이터였던 만큼 mysql보다는 빠른 레디스를 사용하였다. 안정성이 떨어지는 만큼 중요한 데이터를 오래 보관하기 힘든 레디스지만 이렇게 잠깐 저장할 데이터는 빠른 처리가되어 확실히 처리가 용이했다.


 

회고

확실히 팀 프로젝트는 난이도가 높다라는 생각이 든다. 사실 처음 사용하는 레디스와 노드가 많은 데이터들을 외래키로 불러오는 작업이 어려웠지만 이 부분은 사실 비교적 쉬웠다.

 

오히려 git을 쓰면서 생기는 문제나 사람들이랑 계속 소통하는 방식이 새로운 관점을 제시해 준다는 점에서 좋았지만 확실히 여러므로 데이터를 덮어씌워서 만들던게 날아간다거나, 프로젝트가 꼬여서 과거에 복사해둔 프로젝트를 사용했는데 깃 히스토리가 없어서 git pull 을 못하게 되었다든지, 참 여러므로 문제가 많았다.

 

새로운 기술보다도 사람들과 잘 소통할 수 있는 기술의 이해와 개념을 가지고 있어야한다는 것을 절실히 느낄 수 있었다.

 

회고는 아래의 KPT에 추가되는 내용과 일치한다. 하지만 가장 큰 요점은 새로운 기술의 도전과 결합력, 응집력에 대한 고찰이 정말 중요하다.


Keep (유지할 점)

  • 강성욱
    • 빨리빨리 만들기(?)
    • 팀원들을 존중하는 마음
  • 이경훈
    • 프로젝트에 책임감을 갖는 것
    • 새로운 시도를 해보는 것
  • 이하영
    • QueryDsl, 팀장이라는 새로운 시도를 해본 점. 앞으로도 새로운 것을 배우는 자세를 유지하고 싶다.
    • 일정을 일부러 타이트하게 짠 것.
  • 이진영
    • 역할분담과 계획을 같이 세우고 프로젝트를 시작하는 부분이 좋았다
  • 김창현
    • 클린 코드를 지향하는 자세
    • 포기하지 않고 끝까지 책임을 다하는 자세

Problem (개선이 필요한 점)

  • 강성욱
    • 시간을 최대한 확보해 팀원들과 코드 리뷰!!
    • 간결한 프로젝트 구조에 대해 생각해보자!!
    • 테스트 코드 이외에도 테스트 데이터를 깔끔하게 만들어, 테스트를 빠른 시간 안에 할 수 있도록 생각해보자.
  • 이경훈
    • ERD, API를 제대로 만들지 않아서 생긴 문제가 많았다. 추후 수정하는 과정에서 시간을 너무 많이 잡아 먹었다.
    • 새로운 시도의 경우 시도 하는 도중에 기록을 같이 하는 것이 좋다. 추후 다시 볼 때 뭔지 잊는 경우가 많았다.
    • 맨 처음에 공용으로 설정한 부분에 대해서는 정확히 이해하고 함부로 수정하지 말아야한다.
    • 깃의 경우 사용에 문제가 있을 때가 많으니 조그만한 부분이라도 PR을 올리는게 좋다. 중간 저장 안해서 한번에 올릴 때 충돌이 많이나면 수정하기 힘들다.
    • 새로운 기술을 추가하고 싶으면 미리 공부하고 적용하는게 좋다. 갑자기 추가하니 일정 맞추기 힘들다
    • 폴더에 경우 DOMAIN을 기준으로 하기보단 기능을 기준으로 나누는게 더 좋을 것 같다.
    • 다른 파트의 내용을 흡수할 시간이 부족했다.
  • 이하영
    • 5분 기록 보드 등 프로젝트를 하면서 기록을 거의 안 했다ㄷ
    • 타당한 이유가 부족한 점
    • 코드 리뷰 할 시간이 많지 않았던 점
    • 프로젝트 관리 등에서 꼼꼼하지 못한 점 (요구사항 확인 등)
    • 깃, 기술 등에서 아직 알아야 할 것이 많다.
    • 프로젝트가 커질 수록 패키지 구조를 어떻게 나누는게 좋을지 고민해봐야 될 듯
  • 이진영
    • API명세서에 오타가 있었고, 프로젝트 진행중에 API에 변경점이 있었는데 바로바로 고치도록 노력해야겠다
  • 김창현
    • 구조에 대한 생각을 더 해보는 것이 필요할 듯 하다.
    • 성능 개선을 더 고려해야 할 것 같다.

Try (해결책, 시도할 점)

  • 강성욱
    • 서버에 배포하기!
    • 깃허브로 푸시한 이후, 테스트가 정상적으로 완료되면, 자동으로 배포되는 과정까지 경험!!
    • API 문서화 조금 더 신경쓰기!!
  • 이경훈
    • ERD, API 명세서는 고민을 제대로 해볼 것
    • 새로운 시도를 할 경우 소통을 더 중시할 것
    • 시도하기 전에 뭔지 알고, 얼마나 걸릴지 파악해 둘 것
    • 폴더는 기능을 기준으로 나눌 것
    • 모든 기능의 Entity, Reository의 경우 처음에 미리 만들어서 공유하는게 좋다.
    • 복사 붙여넣기 하지마라, 히스토리 날라간다
    • 기능끼리 연관관계가 깊어질수록, 코드를 구성하기 힘들다. 우선은 새로운 기술 사용에 힘을 써보는 것을 목표로 하는 것이 좋을 것 같다.
    • 이번 프로젝트에서 가장 힘들었던 점은, 새로운 기술의 적용으로 생긴 에러, 여러 기능과 연동으로 CRUD 제작에 어려움이 생긴 점이였다. 이 2개의 사용법과 팁을 반드시 기록해 둘 것
    • 5분 기록보드를 쓰는 것이 학습에도 유리할 것 같다. 앞으로는 자주 사용하도록 하자
  • 이하영
    • TIL을 쓰진 못하더라도 조금이라도 시간 내서 5분 기록보드를 써보자
    • 코드 리뷰 하는 시간을 억지로라도 만들어서 하면 좋을 것 같다.
    • 짬날 때 놀지 말고 튜터님이 추천한 유튜브를 보며 지식을 쌓아보도록 노력해야겠다.
  • 이진영
    • 프로젝트 요구사항을 자주보자.
    • 프로젝트 관련 문서들을 자주 보고 잘못되었거나 수정해야할 부분이 있으면 꼭 팀원과 공유 할 것
  • 김창현
    • 프로젝트의 문서화(시각화)를 더 신경써야 할 것 같다.
    • 프로젝트의 기반을 다지는 것에 더 힘써봐야겠다.
    • 테스트 코드를 중요한 기능에는 바로바로 적용할 수 있도록 해봐야겠다.

'Note > 노트' 카테고리의 다른 글

스타터 노트  (1) 2024.10.31
벡엔드 드가자  (0) 2024.10.31

+ Recent posts