스프링과 JPA를 이용한 웹개발 프로젝트_Repository구현
Entity를 만들고 MySQL과 연동해 데이터까지 넣어줬고, Controller를 구현했으니 이젠 Repository와 Service를 구현할 차례입니다!
SeedStarterRepository
public interface SeedStarterRepository extends JpaRepository<SeedStarter,Long> {
}
Jpa를 상속해 기본적인 Jpa들은 사용할 수 있습니다.
ex) findAll, findById ....
SeedStarterService
@RequiredArgsConstructor
@Service
public class SeedStarterService {
private final SeedStarterRepository seedStaterRepository;
public List<SeedStarter> findAll(){
return this.seedStaterRepository.findAll();
}
}
➕@RequitredArgsConstructor를 활용해 의존성을 자동으로 주입해줍니다.
SeedStarterRepository의 findAll 메소드를 정의하고 사용해볼까요?
SeedStarterMngController
@RequiredArgsConstructor
@RestController
public class SeedStarterMngController {
private final SeedStarterService seedStarterService;
@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter, Model model)
{
List<SeedStarter> all = seedStarterService.findAll();
all.stream().forEach(v-> System.out.println("v.getId() = " + v.getId()));
return "hello world";
}
}
그리고 findAll() 값을 List Type all에 담고, stream으로 변환하고, forEach문으로 모든 값을 print하도록 합니다
➡️http://localhost:8080/으로 get 요청을 보내고 나면 인텔리제이에 결과값이 사진과 같이 출력됩니다.
SeedStarterMngController
그러나 우리는 JSON 방식으로 데이터를 요청, 응답하기 때문에 return "hello, wolrd" 부분을 수정해야합니다.
@RequiredArgsConstructor
@RestController
public class SeedStarterMngController {
private final SeedStarterService seedStarterService;
private final ObjectMapper mapper;
@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter, Model model)throws JsonProcessingException
{
List<SeedStarter> all = seedStarterService.findAll();
all.stream().forEach(v-> System.out.println("v.getId() = " + v.getId()));
return mapper.writeValueAsString(all);
}
}
- private final ObjectMapper mapper;를 선언하고
- return mapper.writeValueAsString();에 all을 넣어주면 all 값이 JSON으로 바뀌어 화면에 뿌려집니다!
그러나 지금은 무.한.참.조 StackOverflowError가 발생합니다.
양방향 참조(Bidirectional Relationship)인 인스턴스를 JSON으로 변환하면 무한 재귀가 발생!
@JsonManagedReference, @JsonBackReference을 통해 해결해줍니다.
어노테이션를 적는 기준은 mapping되는 entity에 @JsonManagedReference를, 연관관계의 주인이 되는 entity에 @JsonBackReference를 적어주면 됩니다.
SeedStarter
public class SeedStarter {
@OneToMany(mappedBy = "seedStarter",cascade = CascadeType.PERSIST, orphanRemoval = true)
@JsonManagedReference
private List<Feature> features = new ArrayList<>();
@OneToMany(mappedBy = "seedStarter",cascade = CascadeType.PERSIST, orphanRemoval = true)
@JsonManagedReference
private List<Detail> details = new ArrayList<>();
SeedStart는 PK를 가지고 있고, 일대다 연관관계 중 "일"이기 때문에 Owner 주인이 아닙니다.
➡️주인이 아니기 때문에 managed되고 있다는 뜻으로 @JsonManagedReference을 걸어줍니다.
Feature
public class Feature {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="SEED_STARTER_ID")
@JsonBackReference
private SeedStarter seedStarter;
➡️연관관계의 주인이기 때문에 @JsonBackReference
Detail
public class Detail {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "SEED_STARTER_ID")
@JsonBackReference
private SeedStarter seedStarter;
➡️연관관계의 주인이기 때문에 @JsonBackReference
결과화면 다시보기
저는 크롬 확장자로 JSON을 깔았기 때문에 예쁘게 보여지는 거에요
이제 데이터가 JSON타입으로 잘 반환되는 것을 확인할 수 있습니다!
Postman으로 확인하기
응답으로 수신한 JSON을 크롬 확장 프로그램을 이용하여 가독성이 좋은 형태로 볼 수 있지만 Postman을 이용하여 수신한 JSON을 확인할 수 습니다.
'
N+1 문제 해결하기
fetchType을 LAZY로 적어줬지만 JSON으로 변환하기 위해 연관관계 엔티티를 찾으면서 N+1문제가 발생합니다.
- 쿼리를 보면 SeedStater를 찾을 때 조인이 발생하지 않음
문제 해결을 위해! EntityGraph를 사용하면 됩니다.
SeedStarter
@NamedEntityGraph(name = "SeedStarter.all", attributeNodes = {
@NamedAttributeNode("features")
})
➡️EntityGraph name을 지정하고, attributeNodes에 조회하고싶은 엔티티를 NamedAttributeNode에 적어주면 돼요
Repository
@Repository
public interface SeedStarterRepository extends JpaRepository<SeedStarter,Long> {
@EntityGraph(value = "SeedStarter.all", type = EntityGraphType.LOAD)
@Query("SELECT DISTINCT s FROM SeedStarter s")
List<SeedStarter> findWithFeatureAndDetail();
}
JPQL 쿼리를 통해 가져올 엔티티를 적어주고, 쿼리 이름도 적어줍니다
Service
private final SeedStarterRepository seedStarterRepository;
public List<SeedStarter> findWithFeatureAndDetail(){
return this.seedStarterRepository.findWithFeatureAndDetail();
}
Controller
@RequestMapping({"/", "/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter, Model model) throws JsonProcessingException {
List<SeedStarter> findWithFeatureAndDetail = seedStarterService.findWithFeatureAndDetail();
...
MultipleBagFetchException 에러가 발생합니다.
➡️feature와 detail 쿼리를 동시에 조회하려고 해서 발생하는 에러이기 때문에 이 두가지 entity을 분리해서 조회해야해요
NamedEntityGraphs
쿼리 두 개로 분리해서 각각 조회
@Getter
@Setter
@NamedEntityGraphs(
{
@NamedEntityGraph(name = "SeedStarter.withFeature", attributeNodes = {
@NamedAttributeNode("features")
}),
@NamedEntityGraph(name = "SeedStarter.withDetail", attributeNodes = {
@NamedAttributeNode("details")
})
}
)
@Entity
public class SeedStarter {
NamedEntityGraphs 안에 NamedEntityGraph를 분리해서 넣어주면 됩니다.
당연히 각 EntityGraph 이름도 따로 지어줘야겠죠?
Repository
@Repository
@RequiredArgsConstructor
public class SeedStarterService {
private final SeedStarterRepository seedStaterRepository;
public List<SeedStarter> findWithFeature(){
return this.seedStaterRepository.findWithFeature();
}
public List<SeedStarter> findWithDetail(){
return this.seedStaterRepository.findWithDetail();
}
}
Repository도 각 쿼리를 새로 지어줍니다.
Service
@Service
@RequiredArgsConstructor
public class SeedStarterService {
private final SeedStarterRepository seedStarterRepository;
public List<SeedStarter> findWithFeature(){
return this.seedStarterRepository.findWithFeature();
}
public List<SeedStarter> findWithDetail(){
return this.seedStarterRepository.findWithDetail();
Service에도 다시 메소드를 정의합니다.
Controller
public class SeedStarterMngController {
private final SeedStarterService seedStarterService;
private final ObjectMapper mapper;
@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter, Model model)throws JsonProcessingException
{
List<SeedStarter> seedStarterWithFeature = seedStarterService.findWithFeature();
List<SeedStarter> seedStarterWithDetail = seedStarterService.findWithDetail();
model.addAttribute("seedStarterWithFeature",seedStarterWithFeature);
model.addAttribute("seedStarterWithDetail",seedStarterWithDetail);
return "seedstartermng"; //view 값 반환
}
}
controller에서의 값들을 바로 model 객체에 저장해 view에 JSON 형식으로 전달하도록 수정해줍니다!
model.addAttribute("식별자 이름", 실제값);을 넣어주면, seedatartmng라는 view에 식별자 이름으로 값이 전달됩니다.
EntityGraphType
EntityGraph.EntityGraphType.FETCH
- entity graph에 명시한 attribute는 EAGER로 패치
- 나머지 attribute는 LAZY로 패치
EntityGraph.EntityGraphType.LOAD
- entity graph에 명시한 attribute는 EAGER로 패치
- 나머지 attribute는 entity에 명시한 fetch type이나 디폴트 FetchType으로 패치