[LG CNS AM Inspire Camp 1기] SpringBoot (7) - JPA 사용기

정성엽·2025년 2월 7일
1

LG CNS AM Inspire 1기

목록 보기
41/53

INTRO

이번 포스팅에서는 이전에 MyBatis 기반으로 동작하던 Spring Boot 구조를 JPA로 변경하면서 배운 내용을 정리해보려고 한다.

우선 JPA를 사용하게 된 배경을 살펴보자.

MyBatis를 사용할 때는 Controller, Service, Mapper에서 자바 코드로 DTO를 생성하고 관리했지만, SQL 작성 시 불일치가 발생했다
(예: hitCnt ≠ hit_cnt).

이러한 문제를 해결하기 위해 DTO를 그대로 DB에 매핑할 수 있는 JPA를 도입하게 되었다.

JPA 도입 과정에서 알아야하는 개념과 예시 코드를 정리해보자 👀


1. ResponseEntity

ResponseEntity는 HTTP 응답을 추상화한 객체로, HttpEntity를 상속받은 클래스다.

이는 HTTP 상태 코드, 헤더, 바디를 포함할 수 있어 세밀한 HTTP 응답 제어가 가능하도록 도와준다!

우선 예시 코드를 살펴보자

Sample Code

@GetMapping("/board/{boardIdx}")
public ResponseEntity<Object> openBoardDetail(@PathVariable("boardIdx") int boardIdx) {
    BoardDto boardDto = boardService.selectBoardDetail(boardIdx);
    if(boardDto == null) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", HttpStatus.NOT_FOUND.value());
        result.put("message", "게시물이 존재하지 않습니다.");
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result);
    }
    return ResponseEntity.ok(boardDto);
}

예제 코드에서도 확인할 수 있지만, ResponseEntity는 status, body 등을 설정할 수 있다.

그렇다면 왜 ResponseEntity를 사용하게 되었을까?

자바의 특성상 메서드 하나는 하나의 값만 리턴할 수 있다.

따라서, 여러 정보를 한번에 반환해야하는 http 통신 특징에 맞춰 사용하는 클래스가 바로 ResponseEntity인 것이다.

ResponseEntity는 다음과 같은 구성 요소를 갖는다.

ResponseEntity의 구성요소

  • HTTP Status Code (상태 코드)
  • HTTP Headers (헤더)
  • Response Body (응답 본문)

개발자는 API 명세에 맞게 ResponseEntity를 반환하도록 컨트롤러를 구성하면 된다.


2. Entity

우리가 JPA를 사용하는 가장 큰 이유 중 하나는 자바 객체와 데이터베이스 테이블 간의 불일치를 해소하는 것이다.

이 때, 엔티티는 데이터베이스 테이블에 매핑되는 자바 클래스를 의미한다.

코드를 통해 엔티티의 기본적인 구조를 살펴보자

💡 엔티티의 기본 구조

Sample Code

@Data
@Entity  // JPA 엔티티임을 명시
@Table(name = "t_jpa_board")  // 매핑할 테이블 지정
@DynamicUpdate  // 변경된 필드만 업데이트
public class BoardEntity {
    @Id  // 기본키 지정
    @GeneratedValue(strategy = GenerationType.AUTO)  // 키 생성 전략
    private int boardIdx;

    @Column(nullable = false)  // 컬럼 제약조건 설정
    private String title;
}

위 주석처리된 어노테이션은 기본적으로 엔티티를 구성할 때 사용하는 어노테이션들이다.

엔티티를 구성할 때는 여러 JPA 어노테이션을 사용한다.

@Entity@Table 을 통해 해당 클래스가 어떤 테이블과 매핑되는지 지정하고, @Id@Column 을 통해 각 필드의 속성을 정의한다.

특히 모든 테이블에는 Primary Key가 필요한데, 이는 @Id 어노테이션으로 지정할 수 있다!

💡 주요 어노테이션 설명

@Column 속성

  • nullable: null 허용 여부
  • length: 문자 길이 제약조건
  • updatable: 수정 가능 여부
  • unique: 유니크 제약조건

@GeneratedValue 전략

  • AUTO: JPA가 자동으로 적절한 전략 선택
  • IDENTITY: 데이터베이스에 위임 (MySQL의 auto_increment)
  • SEQUENCE: 데이터베이스 시퀀스 사용
  • TABLE: 키 생성 테이블 사용

각 어노테이션은 엔티티와 테이블을 매핑할 때 다양한 속성을 지정할 수 있다.

@Column 을 통해 각 필드의 제약조건을 설정할 수 있으며, @GeneratedValue 를 통해 기본키의 생성 전략을 결정할 수 있다.

이러한 속성들은 실제 데이터베이스 테이블의 컬럼 속성과 일치하기 때문에 잘 이해하고 있어야 한다!

💡 엔티티 간의 관계 매핑

우선 필자가 생성한 엔티티 코드를 살펴보자

Sample Code

@Entity
@Table(name = "t_jpa_board")
@Data
@DynamicUpdate
public class BoardEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int boardIdx;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String contents;

    @Column(nullable = false)
    private int hitCnt = 0; // 기본값 설정

    @Column(nullable = false, updatable = false)
    @CreationTimestamp
    private LocalDateTime createdDt;

    @Column(nullable = false, updatable = false)
    private String createdId;

    @UpdateTimestamp
    private LocalDateTime updatorDt;

    private String updatorId;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "board_idx")
    @JsonManagedReference
    private List<BoardFileEntity> fileInfoList;
}

@Entity
@Table(name = "t_jpa_file")
@Data
@DynamicUpdate
public class BoardFileEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int idx;

    @Column(nullable = false)
    private String originalFileName;

    @Column(nullable = false)
    private String storedFilePath;

    @Column(nullable = false)
    private long fileSize;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_idx")
    @JsonBackReference
    private BoardEntity board;
}

게시판 시스템을 예로 들면, 하나의 게시물에는 여러 개의 첨부파일이 존재할 수 있다.

이러한 1:N 관계를 JPA에서는 @OneToMany@ManyToOne 어노테이션을 통해 표현한다.

게시물과 첨부파일은 별도의 테이블로 관리되지만, 이 어노테이션들을 통해 두 테이블 간의 관계를 객체 지향적으로 표현할 수 있다.

따라서, 위 코드에서 이 어노테이션들을 사용하여 설정된 내용은 다음과 같다.

@ManyToOne / @OneToMany

  • 하나의 게시글은 여러 파일을 가질 수 있음
  • 하나의 파일은 하나의 게시글에만 속할 수 있음
  • 게시글을 기준으로 파일을 조회할 수 있으며, 파일을 기준으로 게시글을 조회할 수 있다.

테이블의 관계는 프로젝트 설계에 따라 변화할 수 있다.

만약, 파일을 기준으로 게시글을 조회할 필요가 없다면, BoardFileEntity에서 @ManyToOne 은 제거해도 문제가 되지 않는다.

이렇게 설정하면 오직 Board -> BoardFile을 조회하는 단방향 관계를 갖게 된다.

💡 Fetch 전략

다음으로 엔티티 간의 관계를 설정할 때, fetch 전략을 설정하게 된다.

크게 2가지 방법이 존재하는데, 이에 대한 설명은 다음과 같다.

즉시 로딩 (EAGER)

  • 엔티티를 조회할 때 연관된 엔티티도 함께 조회
  • 작은 규모의 데이터나 항상 함께 사용되는 경우에 적합
@OneToMany(fetch = FetchType.EAGER)

지연 로딩 (LAZY)

  • 연관된 엔티티를 실제 사용할 때 조회
  • 대량의 데이터나 선택적으로 사용되는 경우에 적합
@ManyToOne(fetch = FetchType.LAZY)

JPA에서는 연관된 엔티티를 조회하는 두 가지 방식을 제공한다.

EAGER는 연관 엔티티를 즉시 함께 조회하는 방식이고, LAZY는 실제로 필요한 시점에 조회하는 방식이다.

예를 들어 게시글을 조회할 때 첨부파일 정보가 항상 필요하다면 EAGER를, 필요할 때만 조회하고 싶다면 LAZY를 사용하면 된다!

💡 Cascade(영속성 전이)

다음으로는 엔티티의 상태 변화를 연관된 엔티티에도 전파하는 옵션인 Cascade이다.

CascadeType 종류

  • ALL: 모든 상태 변화를 전파
  • PERSIST: 저장 시에만 전파
  • MERGE: 병합 시에만 전파
  • REMOVE: 삭제 시에만 전파

Sample Code

@OneToMany(cascade = CascadeType.ALL)
private List<BoardFileEntity> fileInfoList;

Cascade는 엔티티의 변경이 연관된 엔티티에도 함께 적용되도록 하는 옵션이다.

예를 들어 게시글을 삭제할 때 첨부파일도 함께 삭제하고 싶다면 CascadeType.REMOVE나 CascadeType.ALL을 사용하면 된다.

이를 통해 연관된 엔티티들의 생명주기를 함께 관리할 수 있다.

이처럼 JPA는 엔티티를 통해 데이터베이스 조작을 객체 지향적으로 수행할 수 있게 해준다.

특히 엔티티 간의 관계 설정, 조회 전략, 영속성 전이 등 다양한 기능을 이용하면 더욱 효율적인 데이터 관리가 가능하다는 것을 기억하자!


3. @Repository

JPA에서는 데이터베이스 조작을 위한 인터페이스를 Repository라고 한다.

기본적인 CRUD 작업이 이미 구현되어 있어 별도의 SQL 작성 없이도 데이터 조작이 가능하다는 특징이 있다.

우리가 기본적으로 사용할 수 있는 주요 메서드를 정리해보자

CrudRepository 주요 메서드

  • save(): 엔티티 저장 및 수정
  • findById(): ID로 엔티티 조회
  • findAll(): 모든 엔티티 조회
  • delete(): 엔티티 삭제
  • count(): 엔티티 개수 반환

또한 메서드 이름으로 쿼리를 생성하는 쿼리 메서드 기능도 제공한다.

그렇다면 어떻게 호출하여 사용할까?

💡 @Repository 기본 사용

Repository를 사용하기 위해서는 CrudRepositoryJpaRepository 인터페이스를 상속받고 @Repository 어노테이션을 추가해주면 된다.

Sample Code

@Repository
public interface JpaBoardRepository extends CrudRepository<BoardEntity, Integer> {
    List<BoardEntity> findAllByOrderByBoardIdxDesc();
}

위에서 findAllByOrderByBoardIdxDesc 는 메서드 구현부가 존재하지 않는다.

이처럼 메서드명으로 쿼리를 생성하는 방법을 '쿼리 메서드' 라고 한다.

이 때 사용할 수 있는 주요 키워드는 다음과 같다.

주요 키워드

  • findBy: 조회
  • countBy: 개수 조회
  • deleteBy: 삭제
  • OrderBy: 정렬
  • And, Or: 조건 조합

💡 @Query 사용

물론, 복잡한 쿼리나 성능 최적화가 필요한 경우 직접 쿼리를 작성할 수 있다.

다음 코드를 살펴보자

Sample Code

@Query("SELECT file FROM BoardFileEntity file WHERE file.board.boardIdx = :boardIdx AND file.idx = :idx")
BoardFileEntity findBoardFile(@Param("boardIdx") int boardIdx, @Param("idx") int idx);

위 방법은 SQL이 아닌 JPQL을 사용한 방법이다.

JPQL의 특징은 메서드의 매개변수로 전달받은 내용을 사용하기 위해 : 을 사용한다는 것이다.

이런식으로 우리는 @Repository 어노테이션으로 관리되는 클래스 내부에서 DB에 요청할 쿼리를 정의할 수 있다.

따라서, MyBatis를 사용하는 경우 직접 SQL문을 작성하여 쿼리를 날려야했던 반면, JPA를 사용하는 경우 객체지향적으로 자바 언어를 기반으로 쿼리를 작성하여 정합성을 유지할 수 있다는 장점이 있다!

💡 Service 레이어에서 사용

이제 Repository를 작성했으니 실제 사용을 하는 코드를 정리하려고 한다.

주로 쿼리를 날리는 곳은 Service 레이어이므로 다음과 같이 사용할 수 있다.

Sample Code

@Service
public class JpaBoardServiceImpl implements JpaBoardService{
    @Autowired
    private JpaBoardRepository jpaBoardRepository;

    @Autowired
    private FileUtils fileUtils;

    @Override
    public List<BoardEntity> selectBoardList() {
        return jpaBoardRepository.findAllByOrderByBoardIdxDesc();
    }
    ...
}

@Autowired 로 Repository 의존성을 주입한 이후, 이전에 설정한 쿼리를 호출하여 DB에서 데이터를 가져올 수 있다.


OUTRO

이번 포스팅에서는 Entity와 Repository, 그리고 REST API에서 응답을 보내기 위한 ResponseEntity에 대해 정리해봤다.

MyBatis를 사용하는 경우 발생하는 정합성 문제를 JPA는 자바기반으로 쿼리를 작성하는 방식으로 해결하고 있다.

이번 포스팅을 통해 Entity와 Repository에 대한 내용을 정리할 수 있으면 좋겠다 👊

profile
코린이

0개의 댓글

관련 채용 정보