list -> array
1 | list.toArray(new String[0]); |
toArray의 인자로 변환하고 싶은 array 타입의 변수를 성생해주면 된다.
java 1.6 이전에는 인자로 생성하고 싶은 array 개수만큼 사이즈를 주는게 좋았으나,
1.6 이후로는 0으로 주나 new String[list.size()]
하나 동일하다고 한다
stream -> array
1 | Stream.toArray(String[]::new) |
더 많은 것을 기억하기 위해 기록합니다
maven, gradle
group id = 그룹 아이디, 예를 들면 스프링
artifact id = 모듈 이름, 예를 들면 스프링 시큐리티, 스프링 MVC
액션 검색 : meta + shift + a
새로 만들기 : meta + n
현재포커스 실행 : ctrl + shift + r
이전포커스 실행 : ctrl + r
라이브 템플릿
메인메서드 : psvm
System.out.println : sout
라인 복제하기 : meta + d
라인 삭제하기 : meta + delete
문자열 라인 합치기 : ctrl + shift + j
라인 단위로 옮기기 :
파라미터 즉시보기 : meta + p
코드 구현부 즉시보기 : option + space
docs 보기 : f1
단어별 이동 : alt + 좌우(선택 : +shift)
라인 첫/끝 : fn + 좌우(선택 : +shift)
page up/down : fn + 위아래
포커스 범위(선택) 한 단계씩 늘리기 : alt + 위아래
포커스 앞/뒤 : meta + [/]
멀티포커스 : alt + alt + 위/아래(리눅스는 ctrl)
오류라인 자동 포커스 : f2
변수추출
똑같은 값들을 하나의 변수로 추출하는 과정
추출하고자 하는 값을 선택한 뒤, command + option + v
파라미터 추출
command + option + p
변수가 아니라 파라미터로 추출된다
extract via overloading method를 사용하면 추출된 메서드를
메서드 추출
추출하고 싶은 만큼 코드를 선택한 다음 command + option + m
이너클래스 추출
이너클래스가 여러군데서 사용될 떄 외부클래스로 추출할 수 있다
f6 번을 누르면 어떻게 이동시킬지 선택할 수 있는 부분이 나오고, 여기서 이동할 패키지를 지정해주면 깔끔하게 클래스가 이동된다
이름 일괄 변경
shift + f6
변수 이름 외에, 메서드 이름, 클래스 이름 모두 적용 가능
타입 일괄변경
파라미터 리턴 타입에 마우스대고 cmd + shift + 6
반환하는 값에 대해서는 자동 변환시키거나 직접 설정가능
사용하지 않는 import 제거
ctrl + option + o
command shitf a + optimize import 부분을 off -> on으로 바꾸면 자동으로 사용하지 않는 import를 정리해준다
파일을 열떄마다 자동으로 사라지게 해준다
이 기능을 사용하면 import문을 * 으로 변경하는 경우가 많은데,
action -> import with * 의 개수를 999로 설정하면 해결된다
정렬되지 않은 코드 정렬
command + option + l
메서드 기능이 너무 단순해서 메서드명만 봐도 너무 뻔할 땐,
그 메서드의 기능을 호출하는 메서드에 넣어버리고 그 메서드는 삭제하자
어떤 코드를 그룹으로 묶어도 되겠다고 판단되면,
그 코드를 뺴내어 목적을 잘 나타내는 직관적 이름의 메서드로 만든다
잘
해야겠지… 명확하게추출한 메서드에서만 사용되는 값일 경우
원본 메서드, 추출한 메서드 양쪽에서 다 사용되는 지역변수인데 추출한 메서드에서는 읽히기만 할 경우
원본 메서드, 추출한 메서드 양쪽에서 다 사용되는 지역변수이고 추출한 메서드에서 값이 변경될 경우
변경되는 지역변수가 1개일 경우 리턴해주는 형식으로 작성 가능
1 | int a = 0; |
는 아래처럼 변경 가능
1 | int a = doSomething(0); // 필요한 값을 매개변수로 전달해줄 수 있음 |
2개 이상의 변수가 변경되는 경우, 메서드가 하나의 값을 반환하도록 더욱 분리해주는 것이 좋다.
가능하면 하나의 값만 반환하는 것이 좋다고 한다.
임시변수가 너무 많으면 임시 변수를 메서드 호출로 전환
같은 것을 사용해서 임시변수의 수를 줄이는 것이 좋다.
최대의 구린내는 누가 뭐라해도 중복 코드이다
코드가 중복되면 한쪽만 수정하고, 다른 한쪽은 수정하지 않는 위험천만한 실수를 저지르기 쉽다
메서드 추출
을 적용해서 겹치는 코드를 별도의 메서드로 분리하고, 그 메서드를 두 곳에서 호출하면 된다
메서드 추출
을 적용해서 중복을 없앤 후,메서드 상향
을 적용하면 된다.
메서드 추출
을 적용해서 같은 부분과 다른 부분을 분리해야 한다
경우에 따라템플릿 메서드 형성
을 적용해야 할 수도 있다
두 알고리즘 중 더 간단한 것을 택해서
알고리즘 전환
을 적용하면 된다
주변 메서드 추출
을 적용한다
중복코드를
클래스 추출
이나모듈 추출
을 적용해서 클래스나 모듈로 떼어낸 후, 그것을 호출한다
두 클래스 중 한쪽에서 다른쪽을 호출할수도 있고, 제 3의 클래스로 추출하고 양쪽에서 호출할수도 있다
메서드가 길수록 이해하기 어렵기 때문에 메서드들은 작은 단위로 쪼개주는 것이 좋다
(최적의 상태로 장수하는 객체 프로그램들을 보면 공통적으로 메서드 길이가 짧다)
하지만 하나의 덩어리가 여러개로 나뉘어졌기 때문에, 읽으려면 화면간 전환이 생기기 되고, 이로 인해 사람의 머릿속에 오버헤드가 생기게 된다
그러므로 메서드의 기능을 한눈에 알 수 있는 메서드명을 사용하여 그 메서드 안의 코드를 분석하지 않아도 되게끔 해야한다
메서드명은 기능 수행 방식이 아니라 목적(기능 그 자체)를 나타내는 이름으로 정해야한다
메서드 호출이 원래 코드보다 길어지는 한이 있더라도, 메서드명은 그 코드의 의도를 잘 반영하는 것으로 정해야 한다
메서드의 크기를 줄이려면 십중팔구는 메서드 추출
기법을 적용해야 한다
메서드에 매개변수와 임시변수가 많을 경우
임시 변수를 메서드 호출로 전환
이나 임시변수를 메서드 체인으로 전환
을 사용하면 대부분의 임시변수는 제거된다매개변수 세트를 객체로 전환
과 객체를 통째로 전달
을 적용하면 간결해진다위의 기법을 적용했음에도 불구하고 여전히 임시변수가 너무 많을 때는 메서드를 메서드 객체로 전환
을 적용한다
조건문과 루프도 메서드로 빼야 한다
조건문 쪼개기
를 적용한다루프를 컬렉션 클로저 메서드로 전환
을 적용한 후, 그 클로저 메서드 호출과 클로저 자체에 메서드 추출
을 적용하면 된다UTC : https://ko.wikipedia.org/wiki/협정_세계시
ISO 8601, 표기법 : https://ohgyun.com/416
java의 기존 Date 에 문제가 많았음
월
파라미터에 1~12가 아닌 값이 들어가도 문제없음. 상수이기 때문이다)월
이 상수 0부터 시작한다그래서 Calendar가 나왔는데, 여전히 문제가 있음
좋은 API는 오용하기 어려워야 하고, 문서가 없어도 쉽게 사용할 수 있어야 한다
그러나 Java의 기본 API는 문서를 열심히 보기 전까지는 제대로 사용하기 어렵다
그래서 jodaTime을 대부분 많이 사용했는데, 자바 8 부터 jodaTime의 유용한 기능들을 java.time 패키지에 넣어서 새로 배포함
이와 관련된 자세한 내용은 아래의 글에 나와있다
https://d2.naver.com/helloworld/645609
상세하게 설명되어 있는 블로그
https://perfectacle.github.io/2018/09/26/java8-date-time/
Timezone을 가지지 않는 시간
여기서 불변객체라 함은 setter 등으로 변경할 수 없는 객체를 말한다
LocalDate는 날짜를 표현하는 불변객체
LocalTime은 시간을 표현하는 불변객체
LocalDateTime은 둘을 합쳐서 표현
of
static method로 연,월,일,시간 등을 받아서 생성할 수 있음
now
메서드로 현재 시간 생성 가능
parse
로 문자열을 그대로 받아 사용 가능. 여기에는 DateTimeFormatter 인스턴스를 전달할 수도 있음
http://www.daleseo.com/java8-zoned-date-time/
LocalDateTime에 타임존이나 시차가 추가되었다고 보면 된다
ZonedId나 ZonedOffset 값을 주면 타임존이나 시차를 적용할 수 있고, 값을 주지 않을 경우 로컬의 기본 타임존 값을 사용한다.
ZonedId 이 부모 클래스, ZonedOffset, ZonedRegion 이 하위 클래스임
시차를 쓰는 곳은 많지않고, ZonedOffset 같은 경우 Summer time 등을 처리하지 못하므로 ZonedId를 쓰는 것이 좋다
ZoneId seoulZone = ZoneId.of("Seould/Asia")
LocalDate, LocalDateTime, Instant를 ZonedDateTime으로 변환할 수 있다
1 | LocalDate localDate = LocalDate.now(); |
이것보다는 OffsetDateTime이 더 선호된다고 함
컴퓨터가 알아보기 쉽게 표현하기 위한 형태이다
유닉스 에포크 시간(1970년 1월 1일 0시 0분 0초 UTC)를 기준으로 특정 지점까지를 초로 표현한 것이다.
나노초(10억분의 1)까지 표현 가능하다
ZonedId를 이용해서 LocalDateTime을 Instant로 바꿀 수 있다
1 | Instant instant = LocalDateTime.now().toInstant(seoulZone); |
https://stackoverflow.com/questions/27813691/how-to-compare-two-instant-based-on-the-date-not-time
https://jojoldu.tistory.com/324?category=635883
https://jojoldu.tistory.com/325?category=635883
https://jojoldu.tistory.com/326?category=635883
배치서비스의 특징
Spring Batch는 Acceuture의 배치 프레임워크를 추상화 한것이다.
Spring의 3대 요소인 DI, AOP, 서비스 추상화를 다 사용가능하다.
Spring Quartz는 스케줄러이고, Spring Batch는 대용량 처리 배치이다.
둘은 완전히 다르다.
보통 Quartz + Batch를 조합해서 사용한다. Quartz가 Batch를 실행시키는 구조이다.
Spring Batch 를 사용하고 싶으면 스프링 부트 어플리케이션 루트에 @EnableBatchProcessing
어노테이션을 써줘야함
Job은 하나의 배치 작업 단위를 뜻함
JOB은 @Configuration에 등록하고, 서비스가 뜨면 실행된다
1 | @Bean |
등록한 Batch Job들을 관리하려면 Spring Batch에서 정의한 메타 테이블들을 사용해야 한다
schema-XXX.sql
파일에 스키마가 들어있다Job 안에 여러 Step이 존재함
Step 안에 Tasklet 또는 Reader & Processor & Writer 묶음이 존재함
Job, Step, Tasklet(ItemReader, ItemWriter 등) 은 전부 스프링 빈이다
지정한 JOB만 실행되도록 하는 방법
application.yml
에spring.batch.job.names: ${job.name:NONE}
를 추가하고,
외부 파라미터로 넘어오는job.name
의 값에 맞춰 배치를 실행한다. 전달되지 않으면NONE
에 의해 아무 배치도 실행하지 않는다.
parameter에--job.name=stepNextJob
의 형태로 입력하면 된다
JOB은 파라미터에 따라 BATCH_JOB_INSTACNE에 저장됨
JOB들의 실행을 관리하는 BATCH_JOB_EXECUTION
JOB들의 실제 로직들인 STEP이 저장된 BATCH_STEP_EXECUTION
BatchStatus, ExitStatus
JOB_EXECUTION, STEP_EXECUTION 테이블을 보면 status, exit_code라는 것이 있다.
status는 job이나 step의 실행 결과를 기록할 때 사용하는 Enum이고,
exit_code는 job이나 step의 실행 후 상태를 나타내는 일반 string이다.
기본적으로 exit_code는 status와 같도록 설정되어 있으나, 커스텀한 exit_code가 있으면 추가할 수 있는 구조이다.
Job 내에 Step 등록 시 호출 Flow를 제어 가능하다(순서, 조건별 분기 등등)
1 | @Bean |
step들의 flow 속에서 분기만 담당하는 애들
ExitStatus 세팅하는 부분을 분리할 수 있다
@JobScope, @StepScope 를 선언하면 빈 생성시점이 해당 scope가 실행되는 시점까지 지연된다
Scope를 이렇게 뒤로 미루면서 얻는 장점이 여러가지가 있다
@JobScope, @StepScope 에서 파라미터(외부/내부)를 받을 수 있다
step에 @JobScope를 선언하고 parameter를 매개변수로 받는 방법과,
tasklet, itemReader 등에 @StepScope를 선언하고 parameter를 매개변수로 받는 방법이 있다.
jobParameter가 필요한 곳에서 사용하도록 해야할듯
예를들어 step 단위에서 parameter가 필요하다면 @JobScope에서 파라미터를 받아야 할 것이고,
tasklet 단위에서 parameter가 필요하다면 @StepScope에서 파라미터를 받아야 할 것이다
jobParameter는 @JobScope(step), @StepScope(chunk) 빈을 생성할때만 사용할수있으며,
step과 chunk의 최상위에는 job이 있다.
즉, job을 생성할때 던진 파라미터를 이용해서 scope bean에서 job parameter로 사용하는 것이다(아마도)
이 scope 빈을 @XXXScope로 생성하지 않고 일반 싱글톤으로 생성하면 job Parameter를 찾을 수 없다
jobParameter는 job으로 부터 오는것이니까
job을 생성(실행)할 때 던진 parameter를 가지고 @JobScope, @StepScope 빈을 생성하며 parameter를 주는 것이다
그러므로 job 코드에는 parameter로 null이 들어가게 된다(… 아마도?)
1 | JobParameters jobParameters = new JobParametersBuilder() |
jobLauncher.run을 따라가보면(SimpleJobLauncher 기준)
jobExecution이 있으면 오류가 발생하고, jobExecution이 없으면 jobExecution을 생성한다
그리고 해당 jobExecution은 async로 실행시킨다(아마도)
chunk 지향 처리란 itemReader, itemProcerssor, itemWriter로 이어지는 형태를 말함
tasklet은 내부에 모든 로직이 다 있다
reader에서 job을 읽어오고, process에서 처리하고 writer로 쓴다
reader, process는 1건씩 처리되고, chunkSize만큼 processing이 완료되면 writer로 한방에 쓴다?
chunk는 배치에서 한번에 트랜잭션으로 처리할 단위이다
pageSize와는 다르다. pageSize는 한번에 조회하는 단위이다.
pageSize와 chunkSize를 같게해야 성능상 좋다
reader로 읽을 수 있는 데이터는 데이터베이스만이 아니라 입력 데이터, 파일, jms 등등 여러가지이다
cursor, paging
spring batch에 사용할 단위들을 bean으로 생성할 수 있고, scope가 Job, Step scope 단위라는 점
그러므로 파라미터를 원하는 타이밍에 자유자재로 받을 수 있고, 병렬처리에도 좋다
chunk
https://www.youtube.com/watch?v=1bTIMHsUeIk
RED에서 시작할 수도 있고, GREEN 에서 시작할 수 있다.
여기서 RED는 버그가 있어서 RED인 것이고, TDD의 RED는 아직 코드가 없기 떄문에 RED이다
이미 구현되어 있는 코드에 테스트를 붙이는 방식
- 순수함수로 만들어져 있다
- 외부 의존성이 전혀 없다
- 인풋과 아웃풋이 명확한 단순한 기능들(유틸리티 같은)
- 중요도가 높은 비즈니스 로직
- 버그가 발견된 부분
- 결합이 낮고 논리는 복잡한 부분
여기서 쉽게 테스트할 수 있는 부분을 분리해서 테스트한다
전달받은 정보를 validate 한 뒤
api를 요청하고, 요청이 끝나면 모달창을 닫는다
api 요청은 테스트하기 너무 어렵지만, 위의 validate는 테스트하기 쉽다
그 부분을 함수로 빼고 테스트를 만든다.
여기서 테스트가 실패하는데, 이는 외부 의존성이 남아있었기 떄문이다.
이를 파라미터로 받는 형태로 바꿔서 독립적인 함수로 변형시킬 수 있다
이제 스팩을 추가하고(나와야 하는 결과들) 해당 테스트가 성공하도록 하고,
리팩토링 하면 된다
Test LAST와 달리 이미 구현되어 있는 코드가 없다
신규 요구사항에 대한 개발을 말한다
불안감소
스펙문서기능
디자인 개선 효과
학습 동기부여
개발 생산성 향상
테스트 안해서 아낀 시간(테스트 작성 시간) < 테스트 안해서 나온 버그 고치는 시간
집중력 향상
TDD는 죽었다
에 대한 켄트백의 말
필요하게 될
거라고 알고 있는
기능을 추가하려는 성향이 있다.테스트 자체가 목적이 되어서(커버리지를 높이자!) 테스트를 막 찍어내는 상황
불필요한 테스트
필요하지만 검증방식이 잘못된 테스트
검증력이 떨어지는 테스트
테스트 제목과 검증의 불일치
테스트를 앞서가는 프로덕션 코드
높은 응집, 낮은 결합
을 충족해야 한다
여러 테이블에 공용으로 저장하는 테이블을 만들어야 한다고 가정해보자.
여러 엔티티라고 해서 리스트를 얘기하는 것이 아니라, 다형성을 얘기하는 것이다.
https://www.concretepage.com/hibernate/hibernate-any-manytoany-and-anymetadef-annotation-example
성능상 가장 주의해야 하는것이 이 N+1 문제이다.
아래와 같은 엔티티가 있다고 가정한다.
1 | @Entity |
(참고로 Order에서 Member를 EAGER로 설정해도 동일하게 N+1은 발생한다)
1 | List<Member> members = |
예전에도 언급했지만, JPA는 fetchType을 전혀 신경쓰지 않고 충실하게 JPQL에 맞춰 SQL을 생성한다.
따라서 Member를 전체 조회하는 쿼리가 먼저 실행되고,
Member의 개수만큼 Order를 조회하게 될 것이다(…)
1 | SELECT * FROM Member; -- if result is 5 |
이처럼 처음 실행한 SQL의 결과 수만큼 추가로 SQL을 실행하는 것
을 N+1 문제라고 한다.
즉시로딩을 지연로딩으로 바꿔도 N+1에서 자유로울수는 없다.
즉시로딩이 아니라서 Member 조회와 동시에 Member 건수만큼 Order를 조회해오진 않겠지만,
Member에서 Order를 사용하는 시점에는 똑같이 불러오게 된다.
1 | for(Member member : membres){ |
members 개수만큼 order 조회 SQL이 실행될 것이다.
즉, 이것도 결국 N+1 문제다.
가장 일반적인 방법이다. SQL 조인을 이용해서 연관된 엔티티를 함께 조회하므로 N+1 문제가 발생하지 않는다.
JPQL은 아래와 같다.
1 | SELECT m FROM Member m JOIN FETCH m.orders |
JOIN으로 같이 조회해서 Member 엔티티의 orders 속성에 초기화 하였기 때문에 더이상 N+1이 발생하지 않는다.
참고로 위 예제는 일대다 조인이므로 결과가 늘어날 수 있다. DISTINCT
를 써줘야한다.
하이버네이트가 제공하는 org.hibernate.annotations.BatchSize
어노테이션을 이용하면 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.
1 | @Entity |
즉시로딩이므로 Member를 조회하는 시점에 Order를 같이 조회한다.
@BatchSize
가 있으므로 Member의 건수만큼 추가 SQL을 날리지 않고, 조회한 Member 의 id들을 모아서 SQL IN 절을 날린다.
1 | SELECT * FROM |
size
는 IN절에 올수있는 최대 인자 개수를 말한다. 만약 Member의 개수가 10개라면 위의 IN절이 2번 실행될것이다.
그리고 만약 지연로딩이라면 지연로딩된 엔티티 최초 사용시점에 5건을 미리 로딩해두고, 6번째 엔티티 사용 시점에 다음 SQL을 추가로 실행한다.
hibernate.default_batch_fetch_size
속성을 사용하면 애플리케이션 전체에 기본으로@BatchSize
를 적용할 수 있다.
1
2 > <property name="hibernate.default_batch_fetch_size" value="5" />
>
연관된 데이터를 조회할 때 서브쿼리를 사용해서 N+1 문제를 해결한다
1 | @Entity |
아래와 같이 실행된다.
1 | SELECT * FROM Member; |
즉시로딩으로 설정하면 조회시점에, 지연로딩으로 설정하면 지연로딩된 엔티티를 사용하는 시점에 위의 쿼리가 실행된다.
모두 지연로딩으로 설정하고 성능 최적화가 필요한 곳에는 JPQL 페치 조인을 사용하는 것이 추천되는 전략이다.
JPA의 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하는 특징이 있다.
하지만 단순 조회 화면에서는 조회한 엔티티를 다시 조회할 필요도 없고, 수정할 필요도 없어서 이때는 스냅샷 인스턴스를 위한 메모리가 낭비된다.
이럴 경우 아래의 방법으로 메모리 사용량을 최적화할 수 있다.
엔티티가 아닌 스칼라 타입으로 모든 필드를 조회하는 것이다.
알다시피 스칼라 타입은 영속성 컨텍스트가 관리하지 않는다.
1 | SELECT m.id, m.name, m.age FROM Member m |
하이버네이트 전용 힌트인 org.hibernate.readOnly
를 사용하면 엔티티를 읽기 전용으로 조회할 수 있다.
읽기 전용이므로 영속성 컨텍스트가 스냅샷을 저장하지 않으므로 메모리 사용량을 최적화 할 수 있다.
1 | Member member = em.createQuery("SELECT m FROM Member m", Member.class) |
스냅샷이 없으므로 member의 값을 수정해도 update 쿼리가 발생하지 않는다.
스냅샷만 저장하지 않는 것이지, 1차 캐시에는 그대로 저장한다
똑같은 식별자로 2번 조회했을 경우 반환되는 엔티티의 주소가 같다
스프링 프레임워크를 사용하면 트랜잭션을 읽기 전용 모드로 설정할 수 있다.
1 | @Transactional(readOnly = true) |
트랜잭션을 읽기 전용으로 설정하면 스프링 프레임워크가 하이버네이트 세션의 플러시 모드를 MANUAL
로 설정한다.
이렇게하면 강제로 플러시 호출을 하지 않는 한 플러시가 일어나지 않는다.
엔티티의 플러시 모드는
AUTO
,COMMIT
모드만 있다.
MANUAL
모드는 하이버네이트 세션에 있는 플러시모드이다. 이는 강제로 플러시를 호출하지 않으면 절대 플러시가 일어나지 않는 특징을 가지고 있다.
하이버네이트 세션은 JPA 엔티티 매니저를 하이버네이트로 구현한 구현체이다.
플러시를 수행하지 않으므로 플러시할 떄 일어나는 스냅샷 비교와 같은 무거운 로직들이 실행되지 않으므로 성능이 향상된다.
(그래도 스냅샷은 그대로 저장하는 듯. 단지 플러시만 일어나지 않는 것 같다.)
물론 트랜잭션을 시작했으므로 트랜잭션 시작, 수행, 커밋의 과정은 이루어진다.
트랜잭션 없이 영속성 컨텍스트만 가지고 엔티티를 조회하는 것을 의미한다.
JPA에서 엔티티를 변경하려면 트랜잭션이 필수이므로, 조회가 목적일 때만 사용해야 한다.
JPA는 기본적으로 아래의 2가지 특성이 있다
1 | EntityManager em = emf.createEntityManger(); |
트랜잭션을 시작하는 부분 없이 엔티티매니저만 가져와서 조회를 수행해도 정상 동작하고, 보다시피 lazy 로딩까지도 동작한다.
참고로 여기서 스프링이
@PersistenceContext
를 통해 가져온 EntityManager를 사용할 경우 예외가 발생한다(org.hibernate.LazyInitializationException)
위처럼 메서드내에서 생성된 entityManager에만 유효하다(@PersistenceContext는 공유되는 애라서 그런가…)
공유되는 애인게 왜 문제가 되지?
스프링을 사용할떄는 OSIV를 사용하거나, 직접 EntityManager를 생성시켜주는 방법을 사용해야 한다
기본적으로 @Transactional 을 통해 트랜잭션이 생성될 때 영속성 컨텍스트를 생성하는 구조이기 떄문이다
트랜잭션을 시작하지 않을거라면 @Transactional 어노테이션을 제거해줘야하는데, 그러면 스프링 입장에서 언제 영속성 컨텍스트를 생성해줘야 할지 알지 못하게 된다
그러므로 OSIV를 사용하지 않을거면 EntityManagerFactory를 가져온 뒤 직접 엔티티 매니저를 생성해줘야만 트랜잭션 없이 읽기가 가능하다
이 과정 없이 트랜잭션 없이 읽기를 사용하려면(@Transactional 어노테이션을 제거해서) OSIV를 켜줘야한다