듣기만하던 CORS (Cross-Origin Resource Sharing)을 드디어 만났다.
Nubes fuse mount 구조 변경을 시도하여 local 에서 frontend에서 backend의 데이터에 접근하면 다음과 같은 오류를 만나게 된다.
Access to XMLHttpRequest at '주소A' from origin '주소B' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
처음에 이 오류를 만났을때는 이해가 안갔지만, 직접 환경을 구성하고 사용하다보니 아주 친절한 설명인 것을 깨달았다.
'주소B' Origin으로부터 '주소A'의 자원에 대한 요청이 차단되었음을 의미한다.
Origin은 간단하게 프로토콜, 주소, 포트번호의 쌍을 의미. ([프로토콜]://[Host의 IP 주소 또는 ULR]:[포트번호])
다른 Origin이라고 하면 프로토콜(HTTP, HTTPS)이 다르거나 주소(a.com, b.com)가 다르고나 포트번호가 다르다는 뜻.
해결방법.
가장 간단한 방법은 Access-Control-Allow-Origin 세팅하는 것.
다른 Origin에서 오는 요청을 허용하도록 응답하는 서버에 알려주는 것.
실무에서는 동일한 망에 Origin을 허용하는 것.
내부 클래스 DTO 문제점
바꿔놓은 내부 클래스를 같은 도메인의 DTO를 같은 Package 내에 클래스들을 놓는 방법으로 변환.
Byte array 문제점.
이미지를 Byte array (Base64)로 던지면 예상보다 오랜 시간이 걸림.
CPU, Memory 사용량이 엄청나게 늘어남.
[스크린 샷]
데이터 변환 방식.
검색 결과, React에서 Image를 byte array로 받는 보편적인 방법은 Base64로 다른 방법이 있겠지만 많이 사용되지 않는 것으로 보임.
HttpConnection 방식.
Base64로 인코딩해서 담는 과정도 시간이 많이 걸림.
기존의 Nubes fuse mount하는 방식을 사용.
Bolb을 붙여서 Src를 안보이게 할 수 있음.
Dev 환경에서 확인하기엔 적절하지만 서비스를 배포하고 성능확인하기에는 어려움이 있음.
동일한 조건으로 비교가 필요해서 Local에서 배포된 서비스의 api를 call 하는 방법으로 성능 비교를 실시.
그 결과, 마이그레이션한 Spring boot의 많은 이미지 데이터를 로드해서 정렬하는 페이지에서 성능 차이가 나타남.
ex) Main , Sample 페이지.
자세하게 비즈니스 로직 중 어느 것에서 많은 시간이 걸리는지 확인하기 위하여 Spring boot의 AOP로 시간 측정을 실시.
code
OneToMany 관계인 Collection에 대하여 join fetch를 통해 최적화 수행.
@Override
public List<Sample> findSampleByPage(int page, int limit) {
return em.createQuery( "select s from Sample s " +
" join fetch s.sampleGroups sg " +
" join fetch sg.group g ")
.setFirstResult((page-1)*limit)
.setMaxResults(limit)
.getResultList();
}
앞서 JPA의 N+1 문제를 해결하기 위하여 Fetch Join을 사용했음.
2개 이상의 OneToMany 테이블에 Fetch Join을 선언했을 때, 이 오류가 발생.
이럴 때, 기존에는 한개의 OneToMany 테이블에만 Fetch Join을 걸고, 나머지는 Lazy fetch 를 하도록 했었다.
그렇지만 이것을 해결하기 위한 방법이 한가지 있다.
Hibernate default_batch_fetch_size
default_batch_fetch_szie 옵션으로 해결.
엔티티의 key 하나하나를 조회로 사용하는 방식이 N+1 문제였음.
1개씩 사용되는 조건문을 in 절로 묶어서 조회하는 방식으로 변경하는 것이 default_batch_fetch_szie옵션.
ex) 옵션 값을 1,000 을 준다면 1,000개 단위로 in 절에 key가 넘어가서 엔티티들을 조회.
자세한 예로 1,000개의 Sample 엔티티를 조회한다면
옵션이 없을때,
Sample 테이블 조회 1번
각 Sample의 Group 조회 쿼리 1,000번
각 Sample의 Attribute 조회 쿼리 1,000번
총 2,001번의 쿼리가 수행.
옵션이 있을때,
Sample 테이블 조회 1번
각 Sample의 Group 조회 쿼리 1번 (1000/1000)
각 Sample의 Attribute 조회 쿼리 1번 (1000/1000)
총 3번의 쿼리가 수행.
옵션 값을 넘지않으면 단일 쿼리로 수행된다.
최적화 순서.
fetch join 으로 쿼리 수 최적화.
컬렉션 최적화.
JPA DTO 직접 조회.
Customized Repository
그동안 기본적인 CRUD를 직접 만들어서 사용했음.
이것은 JPA repository가 제공하는 이점인 기본적인 쿼리들을 쉽게 사용할 수 있다는 것을 제대로 사용하지 못한 것.
그래서 Repository 리팩토링을 진행.
@Repository
public class ExperimentRepository {
@PersistenceContext
private final EntityManager em;
public ExperimentRepository(EntityManager em) { this.em = em;}
public void save(Experiment experiment){
if (experiment.getId() == null) {
em.persist(experiment);
}else{
em.merge(experiment);
}
}
public Experiment findOne(Long id){
return em.find(Experiment.class, id);
}
public List<Experiment> findByMain(){
return em.createQuery("select distinct e from Experiment e " +
" join fetch e.ckpts c " +
" where c.on_main = true", Experiment.class)
.getResultList();
}
public List<Experiment> findAll() {
return em.createQuery("select e from Experiment e ")
.getResultList();
}
기존의 Repository 모든 쿼리를 다 수행.
// 사용자 정의 인터페이스
public interface ExperimentRepositoryCustom {
public List<Experiment> findByMain();
}
// 사용자 정의 구현 클래스
@Repository
public class ExperimentRepositoryImpl implements ExperimentRepositoryCustom{
@PersistenceContext
private final EntityManager em;
public ExperimentRepositoryImpl(EntityManager em) {
this.em = em;
}
@Override
public List<Experiment> findByMain() {
return em.createQuery("select distinct e from Experiment e " +
" join fetch e.ckpts c " +
" where c.on_main = true", Experiment.class)
.getResultList();
}
// 사용자 정의 인터페이스 상속
@Repository
public interface ExperimentRepository extends JpaRepository<Experiment, Long>, ExperimentRepositoryCustom {
}
기본적인 쿼리들을 구현하지않고 JpaRpository 구현체들을 사용하여 간단한 쿼리들을 쉽게 사용.
CORS
듣기만하던 CORS (Cross-Origin Resource Sharing)을 드디어 만났다. Nubes fuse mount 구조 변경을 시도하여 local 에서 frontend에서 backend의 데이터에 접근하면 다음과 같은 오류를 만나게 된다.
처음에 이 오류를 만났을때는 이해가 안갔지만, 직접 환경을 구성하고 사용하다보니 아주 친절한 설명인 것을 깨달았다. '주소B' Origin으로부터 '주소A'의 자원에 대한 요청이 차단되었음을 의미한다.
해결방법.
가장 간단한 방법은 Access-Control-Allow-Origin 세팅하는 것. 다른 Origin에서 오는 요청을 허용하도록 응답하는 서버에 알려주는 것.
내부 클래스 DTO 문제점
바꿔놓은 내부 클래스를 같은 도메인의 DTO를 같은 Package 내에 클래스들을 놓는 방법으로 변환.
Byte array 문제점.
이미지를 Byte array (Base64)로 던지면 예상보다 오랜 시간이 걸림. CPU, Memory 사용량이 엄청나게 늘어남. [스크린 샷]
성능 문제
Django 와 Spring boot의 성능 비교. NCC에서 제공하는 Grafana가 아니라 직접 Grafana를 붙여야해서 러닝 커브를 고려해서 좀 더 간단한 방법으로 실시. Django의 Cprofile, debugging toolbar 를 사용해봄. https://github.com/jazzband/django-debug-toolbar https://pypi.org/project/django-cprofile-middleware/
Dev 환경에서 확인하기엔 적절하지만 서비스를 배포하고 성능확인하기에는 어려움이 있음. 동일한 조건으로 비교가 필요해서 Local에서 배포된 서비스의 api를 call 하는 방법으로 성능 비교를 실시. 그 결과, 마이그레이션한 Spring boot의 많은 이미지 데이터를 로드해서 정렬하는 페이지에서 성능 차이가 나타남. ex) Main , Sample 페이지.
자세하게 비즈니스 로직 중 어느 것에서 많은 시간이 걸리는지 확인하기 위하여 Spring boot의 AOP로 시간 측정을 실시. code
MultipleBagFetchException
Sample entity.
앞서 JPA의 N+1 문제를 해결하기 위하여 Fetch Join을 사용했음. 2개 이상의 OneToMany 테이블에 Fetch Join을 선언했을 때, 이 오류가 발생. 이럴 때, 기존에는 한개의 OneToMany 테이블에만 Fetch Join을 걸고, 나머지는 Lazy fetch 를 하도록 했었다. 그렇지만 이것을 해결하기 위한 방법이 한가지 있다.
Hibernate default_batch_fetch_size
default_batch_fetch_szie 옵션으로 해결.
엔티티의 key 하나하나를 조회로 사용하는 방식이 N+1 문제였음.
1개씩 사용되는 조건문을 in 절로 묶어서 조회하는 방식으로 변경하는 것이 default_batch_fetch_szie옵션.
ex) 옵션 값을 1,000 을 준다면 1,000개 단위로 in 절에 key가 넘어가서 엔티티들을 조회. 자세한 예로 1,000개의 Sample 엔티티를 조회한다면 옵션이 없을때,
옵션 값을 넘지않으면 단일 쿼리로 수행된다.
최적화 순서.
Customized Repository
그동안 기본적인 CRUD를 직접 만들어서 사용했음. 이것은 JPA repository가 제공하는 이점인 기본적인 쿼리들을 쉽게 사용할 수 있다는 것을 제대로 사용하지 못한 것. 그래서 Repository 리팩토링을 진행.
기존의 Repository 모든 쿼리를 다 수행.
기본적인 쿼리들을 구현하지않고 JpaRpository 구현체들을 사용하여 간단한 쿼리들을 쉽게 사용.