@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@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());
}
}
위 코드를 실행하면 오류가 발생한다. 왜냐하면 Question 리포지터리가 findById를 호출하여 Question 객체를 조회하고 나면 DB세션이 끊어지기 때문이다. 그 이후에 실행되는 q.getAnswerList() 메서드는 세션이 종료되어 오류가 발생한다. 답변 데이터 리스트는 q 객체를 조회할때 가져오지 않고 q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문이다.
Lazy 방식 : 필요한 시점에 데이터를 가져온다.
Eager 방식 : q 객체를 조회할때 답변 리스트를 모두 가져온다.
@OneToMany, @ManyToOne 애너테이션의 옵션으로 fetch=FetchType.LAZY 또는 fetch=FetchType.EAGER 처럼 가져오는 방식을 설정할 수 있다.
사실 이 문제는 테스트 코드에서만 발생한다. 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않기 때문이다. 테스트 코드를 수행할 때 위와 같은 오류를 방지할 수 있는 가장 간단한 방법은 @Transactional 애너테이션을 사용하는 것이다. @Transactional 애너테이션을 사용하면 메서드가 종료될 때까지 DB 세션이 유지된다.
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Transactional
@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());
}
}
데이터 조회하여 템플릿에 전달하기
Question 리포지터리로 조회한 질문 목록은 Model 클래스를 사용하여 템플릿에 전달할수 있다.
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionRepository questionRepository;
@GetMapping("/question/list")
public String list(Model model) {
List<Question> questionList = this.questionRepository.findAll();
model.addAttribute("questionList", questionList);
return "question_list";
}
}
Model 객체는 자바 클래스와 템플릿 간의 연결고리 역할을 한다. Model 객체에 값을 담아두면 템플릿에서 그 값을 사용할 수 있다. Model 객체는 따로 생성할 필요없이 컨트롤러 메서드의 매개변수로 지정하기만 하면 스프링부트가 자동으로 Model 객체를 생성한다.
스프링의 의존성 주입(Dependency Injection) 방식 3가지
- @Autowired 속성 - 속성에 @Autowired 애너테이션을 적용하여 객체를 주입하는 방식
- 생성자 - 생성자를 작성하여 객체를 주입하는 방식 (권장하는 방식)
- Setter - Setter 메서드를 작성하여 객체를 주입하는 방식 (메서드에 @Autowired 애너테이션 적용이 필요하다.)
리다이렉트 또는 포워딩
- redirect:<URL> - URL로 리다이렉트 (리다이렉트는 완전히 새로운 URL로 요청이 된다.)
- forward:<URL> - URL로 포워드 (포워드는 기존 요청 값들이 유지된 상태로 URL이 전환된다.)
서비스 클래스가 필요한 이유
- 모듈화
- 어떤 컨트롤러가 여러개의 리포지터리를 사용하여 데이터를 조회한후 가공하여 리턴한다고 가정할 때, 해당 기능을 필요로 하는 모든 컨트롤러가 동일한 기능을 중복으로 구현해야 한다.
- 보안
- 컨트롤러를 리포지터리 없이 서비스를 통해서만 데이터베이스에 접근하도록 구현하면, 어떤 해커가 해킹을 통해 컨트롤러를 제어할 수 있게 되더라도 리포지터리에 직접 접근할 수는 없게 된다.
- 엔티티 객체와 DTO 객체의 변환
- 엔티티 클래스는 데이터베이스와 직접 맞닿아 있는 클래스이기 때문에 컨트롤러나 타임리프 같은 템플릿 엔진에 전달하여 사용하는 것은 좋지 않다. 따라서 Question, Answer 대신 사용할 DTO(Data Transfer Object) 클래스가 필요하다. 서비스는 컨트롤러와 리포지터리의 중간자적인 입장에서 엔티티 객체와 DTO 객체를 서로 변환하여 양방향에 전달하는 역할을 한다.
Spring Boot Validation
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
@Getter
@Setter
public class QuestionForm {
@NotEmpty(message="제목은 필수항목입니다.")
@Size(max=200)
private String subject;
@NotEmpty(message="내용은 필수항목입니다.")
private String content;
}
@NotEmpty : 빈 값을 허용하지 않는다.
@Size(max=200) : 최대 길이가 200 바이트를 넘으면 안 된다.
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "question_form";
}
this.questionService.create(questionForm.getSubject(), questionForm.getContent());
return "redirect:/question/list";
}
@Valid : QuestionForm에서 설정한 검증 기능이 동작한다.
BindingResult : 검증이 수행된 결과를 의미하는 객체. 항상 @Valid 매개변수 바로 뒤에 위치해야 한다.
'프로젝트 & TIL > 일별 공부 기록 (백엔드 스쿨)' 카테고리의 다른 글
23일차 - 점프 투 스프링부트 3장(2) (0) | 2023.03.23 |
---|---|
22일차 - 점프 투 스프링부트 3장 (0) | 2023.03.22 |
20일차 - 점프 투 스프링부트 (0) | 2023.03.20 |
19일차 - 스프링부트와 DB 연결하기, Spring Data JPA (1) | 2023.03.17 |
18일차 - 스프링부트, 세션, thymeleaf (0) | 2023.03.16 |