스프링

스프링과 JPA를 이용한 웹개발 프로젝트_Repository구현

JANNNNNN 2024. 4. 8. 11:26

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);
    }
}
  1. private final ObjectMapper mapper;를 선언하고
  2. 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으로 패치