질문(1) : 답변(N) 개발 과정 정리

Ryu·2023년 3월 30일
0

[ 엔티티 ]


🍎 미션 [2] :

Quesiton,Answer 엔티티 구성, 양방향관계 구현, 생성일 구현
-Question삭제 시 Answer 모두 삭제 가능
-BaseEntity로 공통 속성 묶어내기(id, 생성일)

@Getter
@Setter
@Entity
public class Question {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // 자동으로 1 씩 증가하는 전략.
    private Integer id;

    @Column(length = 200)
    private String subject;

    @Column(columnDefinition = "TEXT")   // TEXT 는 '내용'과 같이 길이 제한이 없을 때.
    private String content;

    private LocalDateTime createDate;   // 자동으로 테이블에는 create_date로 저장.
		
		@OneToMany(mappedBy = "question", cascade = CascadeType.ALL)  **// 질문 삭제 시, 참조하는 답변들도 모두 삭제된다는 뜻.**
		private List<Answer> answers = new ArrayList<>();
}
  • @ManyToOne 을 통해, FK 관계가 생성된다. (Answer 클래스에서 → Question 참조할 때.)
  • 질문 하나여러 개 답변들이 달린다. 1 : N
    질문이 삭제되면, 답변들도 모두 삭제된다. 따라서, 참조하는 변수 위에 CASCADE 속성.
@Getter
@Setter
@Entity
public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(columnDefinition = "TEXT")
    private String content;

    @CreatedDate
    private LocalDateTime createDate;

    @ManyToOne
    private Question question;
}

[ 리포지터리 ]


리포지터리가 필요한 이유

  • 데이터 처리를 위해서는 테이블에 어떤 값을 넣거나 조회하는 등의 CRUD 가 필요하다.
    → 이 CRUD를 처리하기 위해 DB에 접근하는 계층이 바로 리포지터리.

🍎 미션 :

QuestionRepository 를 만들어보자.
save()에 대한 테스트코드도 추가해보자.
findAll(), findById(), findBySubject(), findBySubjectAndContent()

public interface QuestionRepository extends JpaRepository<Question, Integer> {

}

Spring Data JPA 가 지원하는 다양한 메서드

Spring Data JPA - Reference Documentation

참고). @Autowired (필드 주입)

  • 객체 자동 주입, 의존성 주입이라고도 함.
  • DI 라고도 함. Dependecy Injection. 스프링이 객체를 대신해서 주입해주는 것.
  • 개발 코드에서는 필드주입, setter 주입 말고 생성자 주입을 써야 함.
    그러나, Test 코드에서는 필드 주입 사용.(생성자 주입 사용 불가능.)

🍎 미션 :

제목에 특정 문자열이 포함되어 있는지 조회하는 메서드를 만들어보자.

@Test
    @DisplayName("findBySubjectLike()")
    public void t4() {
        //given
        List<Question> questions = questionRepository.findBySubjectLike("%springboot%");

        //when

        //then
        Assertions.assertThat(questions.size()).isEqualTo(2);
    }
  • sbb% : "sbb"로 시작하는 문자열
  • %sbb : "sbb"로 끝나는 문자열
  • %sbb% : "sbb"를 포함하는 문자열

데이터 수정과 삭제

미션 :

질문데이터를 수정하는 테스트 코드 작성

오류 발생 : Provided id of the wrong type for class com.ll.exam.sbb.Question

id 타입을 Long 으로 해놓고, JPARepository<Question, Integer> 라고 해서 오류 발생.
그래서, id 타입을 Integer라고 수정 했다.

미션 :

답변 데이터를 생성하고 저장, 조회하는 메서드까지 TDD 개발.

에러). Column "start_value" not found 에러 발생.

  • spring.jpa.hibernate.ddl-auto=create 로 create 로 바꿔주니까 해결… update 로 해주려면 ?

👍 해결

  • spring.jpa.hibernate.hbm2ddl.auto=update
  • 스프링 부트 버전 호환 문제 때문에 오류 발생한 것 같다.

데이터 삭제


  • questionRepository.count(); ⇒ 저장소에 남아 있는 데이터 개수.
    아..! 리포지터리에도 count(); 메서드라는 것도 있구나.

답변 데이터 생성 후 저장


@Test
@DisplayName("답변 데이터 생성 후 저장")
public void t6() {
    //given
    Question question = questionRepository.findBySubject("springboot 질문").get();
    Answer answer = new Answer();
    answer.setContent("네 자동으로 생성됩니다.");
    answer.setQuestion(question);
    answerRepository.save(answer);

    //when

    //then
    Answer answer1 = answerRepository.findById(answer.getId()).get();
    Assertions.assertThat(answer1.getContent()).isEqualTo("네 자동으로 생성됩니다.");
}

답변에 연결된 질문 찾기 와 질문에 달린 답변 찾기


// Answer에서 question 들 조회.
@Test
void testJpa() {
    Optional<Question> oq = this.questionRepository.findById(2);
    assertTrue(oq.isPresent());
    Question q = oq.get();

    List<Answer> answerList = q.getAnswerList();

    assertEquals(1, answerList.size());
    assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}

🤔 에러 발생

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.ll.exam.sbb.Question.answerList, 
could not initialize proxy - no Session
  • 원인 : Question 객체를 조회(findById)하고 나면, DB 세션이 끊어지기 때문!!!
    • 그 이후에 사용하는 getAnswerList(); 메서드는 세션이 끊어진 후 사용하기 때문에 에러가 발생한다.
    • 다시 말해, Question(일대다에서 일)을 조회할 때, 바로 Answer(일대다에서 다)을 가져오지 않기 때문!!! ⇒ 이렇게 필요한 시점에 데이터 가져오는 것을 LAZY 방식이라고 한다.
    • @OneToMany 에서는, ‘다’를 가져올 때 한 번에 다 가져오면 많이 가져오기 때문에 LAZY 전략이 Default 이다.
    • @ManyToOne 에서는, ‘일’을 가져올 때 한 번에 다 가져와도 괜찮으므로 EAGER 전략이 Default 이다.
    • 사실 이 문제는 테스트 코드에서만 발생한다!!! (실제 서버에서는 DB 세션 종료되지 않으므로 오류 발생 안함.)
      ⇒ 이를 해결하기 가장 편한 방식은 @Transactional 의 사용이다.
  • 해결 방법 : 2가지가 존재한다.
    • Question(1) 쪽에서 fetch Type 을 LAZY 에서 EAGER 로 변경하거나,
    • 아니면 그냥 테스트 코드에 @Transactional 을 붙이거나. (권장)

테스트 코드에서는, ‘답변’에서 ‘질문’을 조회할 때에도, could not initialize proxy 에러가 발생한다.

@Test
@Transactional
@DisplayName("답변에 연결된 질문 찾기")
public void t7() {
    //given
    Question question = questionRepository.findBySubject("springboot 질문").get();
    Answer answer = new Answer();
    answer.setContent("네 자동으로 생성됩니다.");
    answer.setQuestion(question);
    answerRepository.save(answer);

    //when
    Answer answer1 = answerRepository.findById(answer.getId()).get();
    Question question1 = answer1.getQuestion();
    String subject = question1.getSubject();

    //then
    Assertions.assertThat(subject).isEqualTo("springboot 질문");
}
  • 테스트 코드가 아닌 곳에서는, DB세션이 종료되지 않기 때문에, 영속성 컨텍스트가 유지되어 에러가 발생하지 않는다.
  • Answer(N) 에서도 Question(1)에 대해 ManyToOne(fetch = FetchType.LAZY) 가 걸려 있어서, 프록시 객체로 가져온다.
    • 이를 해결할 방법은, @Transactional 로 간단히 해결할 수 있다.
      @Transactional은 메서드가 종료될 때까지 DB세션이 유지되므로, 영속성 컨텍스트가 유지된다.
      - 엄밀히 말하면, DB세션 종료와 영속성 컨텍스트 종료는 다른 개념이지만 보통 영속성 컨텍스트가 종료된 후 DB 세션이 종료된다. 즉, DB 세션이 살아있으면 영속성 컨텍스트도 살아있을 확률이 높다.
      참고로, 영속성 컨텍스트는 엔티티와 관련된 데이터를 메모리에 캐싱하는 매커니즘이다.

트랜잭션의 시작은 언제 발생하는가 ?

트랜잭션은 @Transactional 또는 EntityManager에 의해 시작할 수 있다.
참고로, Spring Data JPA 의 리포지터리 메서드에는 트랜잭션과 관련한 기능들이 내장되어 있다.

profile
Strengthen the core.

0개의 댓글