해결된 질문
작성
·
693
·
수정됨
1
안녕하세요! 아주 용감하게.. 선생님의 두 강의를 동시에 듣고 있는 학생입니다 ^^
질문이 몇 가지 있는데요,
1) 아직 기본적인 부분도 채워가는 중이라 트랜잭션의 기본 정의부터 검색을 여러모로 해보다가 "데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미"한다는 내용을 발견했는데 이와 관련하여 Transactional annotation을 사용하신 게 맞죠? :)
2) 선생님이 다른 분에게 답변하신 내용 중에 트랜잭션 처리를 Service의 역할로 본다고 하신 의견을 참고 하고, Jpa 연동된 프로젝트랑 Jdbi를 셋업해둔 프로젝트를 살펴봤는데 Jdbi 기반인 코드의 서비스 레벨에서는 Service나 Transactional annotation을 안쓰더라구요. 그게 딱 Jpa에 한정된 내용인가요?
3) 수업 시간에 deleteUser fun을 작성하실 때, 유저를 삭제하는 부분 로직이 들어갔는데요. 만일 제가 delete fun에 여러 테이블의 데이터를 삭제하고자 한 특정 repository의 delete 메소드를 여러 번 호출해서 그 대표 func 안에 두어도 되나요? Jdbi를 사용하는 프로젝트에서는 그렇게 풀어나가는 걸 봤는데 Jpa에서도 @Trasactional 달고 가능한지 궁금합니다. 아님 single responsibility로 fun deleteA, fun deleteB 이렇게 나누는 게 나을지요?
4) 수업 시간에 .map { UserResponse(it) } 과 .map(::UserResponse) 보다는 .map { user -> UserResponse(user) } 를 더 선호하신다고 했는데 그 이유가 뭔가요? 가독성 때문인가요? 다른 두 옵션이 좀 더 스마트해보이고 세련된 syntax 같아 보이는 이상한 생각이.. :)
5) UserService.kt에서 fun getUsers(): List<UserResponse> 위에 @Transactional(readOnly = true)을 붙여주는데 readOnly가 빨간색이 뜨네요. Cannot find a parameter with this name: readOnly라고 에러 내용 확인이 가능한데 이걸 해결하려고 재빌드에다 dependency확인에 다시 IDEA reboot하고 구글검색도 했는데 해결이 안되서 도움 요청 드립니다. 뭔가 엄청 간단한 걸 빼 먹었을 것 같은데 선생님은 확인이 되실까요?
6) 팀 시니어들이 예전에 어떤 프로젝트가 JPA 사용된 걸 보고, 매우 싫어하는 분위기였는데 보아하니 제 팀은 보통 다 Jdbi 사용하네요. 한국에선 JPA 사용이 더 흔한 것 같은데, 혹시 단점을 쉽게 설명해주실 수 있으신가요?
질문이 많아서 죄송해요. 한꺼번에 모아두고 물어보는 게 나은 것 같아서.. ^^
새해 복 많이 받으시고 두 강의 다 마치고 따뜻한 후기 올릴게요. 선생님의 코틀린 심화편도 고대하고 있습니다. 이 두 강의 곧 마치고 얼른 만나보고 싶은데 강의 준비 화이팅입니다!
답변 2
1
답변이 아직 없으신 동안 혼자 ReadOnly 이슈를 풀어보려고 @Transactional 옆에 ReadOnly = true를 지우고 테스트를 돌려도 아래 에러가 나길래 IntelliJ Preference랑 Project Structure 다 확인하고도 소용이 없고 그래서
* What went wrong:
Execution failed for task ':compileTestJava'.
> invalid source release: 11
무모하게 Cache를 전부 다 invalidate했다가 더 큰 문제가 생겼어요 ㅠ
이젠 ./gradlew build를 하면 아래 에러가 떠서
build.gradle에 아래 내용을 추가해야 해당 에러가 정리가 되네요..
그러고 다시 빌드 커맨드를 돌렸더니 맨 처음 invalid source release: 11 가 떠서 고치려고 보고 있습니다. Invalidate all caches가 아니라 그냥 Repaire IDE만 했으면 좋았을텐데 ㅠㅠ 방법이 없을까요?
0
안녕하세요! Suyeon님!!! 아이고~ 이렇게 정성스러운 질문 남겨주셔서 너무너무 감사드립니다!!! 🙇🙇 새해 연휴 이후 정신없이 일하고 퇴근하니~ 벌써 이 시간이네요..!! 조금 더 빠르게 답변 남겨드리지 못해 죄송합니다! 😭 빠르게 하나씩 말씀드려보도록 하겠습니다~!!!
[1. 트랜잭션 어노테이션의 의미]
네네 맞습니다!! 트랜잭션이란, 잘 찾아주신 것처럼, '모두 같이 성공하거나 모두 같이 실패해야 하는 작업의 단위'를 의미합니다! @Transactional
어노테이션을 붙이시게 되면, 해당 어노테이션이 붙은 함수 바깥에서 해당 함수가 호출될 때, 그 함수 안의 작업들은 모두 같이 성공하거나 모두 같이 실패하게 됩니다!!
class OrderService {
@Transactional
fun completePayment() {
orderRepository.save(Order(..))
pointRepository.save(Point(..))
billingHistoryRepository.save(BillingHistory(..))
}
}
예를 들어, 위의 OrderService
에서 completePayment
함수는 @Transactional
이 붙어 있기 때문에, 1) 주문 저장 2) 포인트 저장 3) 결제 기록 저장이 모두 같이 이루어지거나 셋 중 하나라도 실패하면 모두 다 같이 실패하게 됩니다!!
조금 어려운 내용이지만, 해당 어노테이션이 붙은 함수 바깥에서 함수가 호출되어야 한다는 조건은 https://cheese10yun.github.io/spring-transacion-same-bean/ 를 확인해보시면 좋습니다!! 😊
[2. Transaction과 JPA, Jdbi]
Suyeon님게서 질문 주셔서 Jdbi가 무엇인지 조금 찾아봤어요!! 6번 질문과도 관련이 있더라고요 ㅎㅎㅎ 제가 조금 찾아보았을 때 Jdbi는 JDBC보다 조금 더 발전된 형태의 SQL builder로 생각이 되었습니다!! (https://stackoverflow.com/questions/5819392/what-is-the-difference-between-jdbc-and-jdbi, 당연히 단편적으로만 아는 내용이라 틀릴 수도 있습니다!)
@Transactional
어노테이션의 경우 JPA뿐 아니라 JdbcTemplate, MyBatis(iBatis)에서도 활용이 가능합니다! (라이브러리 및 버전에 따라 추가적인 옵션이 일부 필요할 수는 있어요!!) 트랜잭션을 시작하고 관리하는 부분을 서비스 계층의 역할로 생각하는 이유를 조금 더 설명드리면 좋을 것 같은데요! 약간 옛날 얘기부터 시작해보도록 하겠습니다!!!
옛날에는... 그러니까 JPA가 탄생하기 전에는 Jdbc를 사용해 MySQL에 직접적으로 쿼리를 날리는 것이 유일한 방법이었어요!! 당연히 SQL도 직접 작성해주어야 했고요! 하지만 이렇게 SQL을 직접 작성하는 것에는 몇 가지 단점이 있었습니다.
SQL은 문자열로 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다.
특정 DB에 종속적이게 된다.
MySQL에 대한 SQL을 fit하게 문자열로 작성하면 PostgreSQL로 한 번에 갈아타기 어렵죠..!
반복 작업이 많아진다.
데이터베이스의 테이블과 객체는 패러다임이 다르다.
대표적으로 연관관계와 상속관계가 있습니다!
때문에 이런 문제들을 해결하고, 조금 더 객체지향적인 코드를 작성하기 위해 JPA가 등장하게 되었습니다! (물론, 그 사이에 MyBatis같은 SQLMapper도 등장했고요..!!) 이렇게 JPA가 등장하고 난 이후, Spring에서 계층형 아키텍처를 적용했을 때 일반적인 다음 4단계로 나눠지게 됩니다!
Controller
Service
Repository
Domain (Business 관심사를 다루는 객체)
이런 상황에서 API 부분은 Controller에, 비즈니스 로직은 최대한 도메인에, 도메인을 가져오거나 DB와 접근하는 부분은 Repository에 들어가게 되었고요!
자연스럽게 여러 도메인에 동시 접근해야 하는 상황이라면 Service에서 그 흐름을 제어해주고, 적절하게 트랜잭션을 걸어주게 되었죠! 물론 서비스가 복잡해질 수록 한 API에서 여러개의 Service를 사용하기도 합니다! 이때 트랜잭션의 다양한 전파 옵션을 사용하기도 하고요! 👍
ex) AController -> AService -> BService + CService -> 다시 AService -> AController 결과 반환
[3. 여러 도메인을 삭제해야 하는 경우의 코드 위치]
이 부분은 아마 이런 말씀이신 것 같아요!!! 이제 A 도메인과 B 도메인을 제거한다고 해볼게요~!! 이때 다음과 같은 2가지 방법이 있습니다.
// 방법 1
// Service
@Transactional
fun deleteAB() {
aRepository.deleteA()
bRepository.deleteB()
}
// Arepository
fun deleteA() {
// A를 지운다
}
// BRepository
fun deleteB() {
// B를 지운다
}
이 방법은 Service에서 2개의 도메인에 관여하는 방법이죠!
// 방법 2
// Service
fun deleteAB() {
xxxRepository.deleteAB()
}
@Transactional
fun deleteA() {
// A를 지운다
// B를 지운다
}
이 방법은 Repository가 2개의 도메인에 관여하는 방법입니다!
우선 결론부터 말씀드리면 2가지 방법 모두 정상 동작합니다!! Spring Data JPA만 사용하신다면, Repository 안에 자유로운 쿼리 작성에 제한이 있어 조금 어려울 수도 있지만, Querydsl까지 사용하게 되면 정말 자유롭게 함수를 구성하고 @Transactional
어노테이션도 Repository 클래스 내부 함수에 붙일 수 있게 되죠!
하지만 저는 1번 방법을 선호합니다!! 그 이유는...
1. "Service에서 여러 도메인에 대한 흐름 제어"를 해주는 것이 더 자연스럽게 느껴지기 때문입니다!!
둘 모두 성능도 비슷하고 가독성도 크게 다르지 않으니, 더 좋은 방법이란 다른 사람들이 익숙한, 그리고 딱 봤을 때 아~ 이 코드는 여기 있겠네~ 할 수 있는 그런 방법이 될텐데요!! 제가 경험했던 코드들과 함께 했던 분들은 Service에서 여러 도메인에 대한 흐름 제어를 해주시는 것에 대해 더 익숙하시다 보니, 저도 그렇게 느끼는 것 같아요!!
2. 왜 Service에서 여러 도메인에 대한 흐름 제어를 하는 것이 자연스럽게 느껴질까?!
1번과 이어지는 내용으로 매우 개인적인 의견입니다!!
저희가 SW를 개발하며 코드를 작성하다 보면, 결국 여러 개의 객체에 의존해야 하는 상황이 생기게 됩니다. 그리고 동시에 SW에 대한 변경이 이루어졌을 때 side effect를 최소화 하고 싶어 하죠.
이런 관점에서, A와 B를 함께 제거하는 코드가 Service에 있을때, 그리고 Repository에 있을 때를 비교해보겠습니다.
일반적으로 계층형 아키텍처에서 주요하게 다뤄지는 원칙은, 코드의 흐름이 한 방향으로만 가능하다는 점입니다! Controller는 Service를 사용할 수 있지만, Service에서는 Controller를 사용할 수 없고, Service에서는 Repository를 사용할 수 있지만, Repository에서는 Service를 사용할 수 없죠!!
이를 그림으로 표현하면 다음과 같습니다.
좋습니다~!!! 자 이제, 저희가 1번 방법 처럼 Service 계층.. 그 중에 Service 2
에 A와 B를 함께 삭제하는 로직이 작성되었다고 해보겠습니다! 그리고 그 로직을 수정해야 할 일이 생겼어요..! 그 때 영향범위는 어떻게 될까요?! 현재 화살표에서 알 수 있듯이 Controller2와 Service2에 국한될 것입니다!
자 그런데~~ 이번에는 2번 방법처럼 Repository에 A와 B를 함께 제거하는 로직을 작성했다고 해볼게요!! 물론 그 로직을 작성할 초기에는 하나의 Service에서만 활용하고 있을 확률이 높겠지만 사람이 바뀌고, 시간이 흐르면 Service에서는 Repository 로직을 가져다 사용할 수 있기 때문에 그 로직을 여기저기서 가져다 쓰고 있을 겁니다...! 저희가 A Repository
에 그 로직을 작성했다고 해보죠~
그러면 이 로직이 변경될 때의 영향 범위는 무려... 코드 전체가 되게 됩니다!
그래서 아마 많은 분들이 Service 쪽에서 도메인 흐름을 제어하는게 자연스럽다고 생각하시는 것 같아요!! (거듭 강조드리지만 제 생각입니다!) 물론 Service 역시 코드가 복잡해지면 Service 끼리의 의존관계가 생기기도 하지만, 잘 제어하기만 하면 Repository에 도메인 흐름 제어 로직이 들어가는 것보다 훨씬 용이하게 관리할 수 있거든요!
3. 만약 서비스가 확장되어 A와 B를 한 번에 제거하는 대신 '최종적 일관성'을 지켜주는 방식으로 서비스가 발전한다면..?!
최종적 일관성을 지켜주기 위해서는 외부 자원 (API 호출 or Event 전송) 사용이 필요하고 외부 자원을 제어하는 것 역시 Service 계층의 역할입니다! 때문에 원래 B 도메인 제거를 Service에서 해주고 있었다면, 그 부분만 코드를 수정하면 되겠지만, Repository 계층에서 A와 B 도메인 제거를 해주고 있었다면, Repository와 Service 코드 모두 수정이 되기 때문에 수정 범위가 조금 더 클 것 같아요!! 때문에 추후 확장성 측면에서 1번 방법이 조금 더 끌리는 것 같습니다 ㅎㅎㅎ
저는 1번 방법을 선호한다고 말씀드렸지만, 결국 정답이 있다기보다는 함께 협업하시는 분들과의 Align이 중요한 부분이지 않을까 생각됩니다~ 😊😊
혹시 기다리실까봐 여기서 한 번 끊고 4, 5, 6번 댓글로 이어서 말씀드려보겠습니다!!!
[4. 람다를 사용할 때에 변수명을 명시하는 이유]
맞습니다!! 가독성 때문인데요!! 저희가 람다를 사용해서 함수형 프로그래밍을 하다보면, 현재 컬렉션에 담겨 있는 변수가 무엇인지 헷갈리는 경우가 많이 있습니다!
특히 예를 들어 아래 코드와 같이 중첩된 상황에서 it을 남발하거나 계속해서 연산이 이어지는 경우, 람다에 무엇이 들어갔는지 변수명으로 표기해주면 가독성이 좋아지는 경우가 많이 있더라고요!! 😊
list1.forEach {
it.map { it.name }
}
list1.map { }
.groupBy { }
.mapValues { }
.values
.forEach { }
물론, 가독성이란 부분이 주관적이고 상황에 따라 많이 다릅니다!! UserResponse
를 만드는 부분은 로직이 간단하기에 method reference나 it을 활용하더라도 좋을 것 같아요!!! (복잡한 상황까지 가정해서 선호도를 말씀드리느라 이렇게 언급드렸습니다 하하핫..!! 😅)
[5. 트랜잭션 readOnly의 빨간줄]
아이고~~ 엄청 고생하셨군요..!! 🥺🥺 제가 예상하기로는 @Transactional
어노테이션의 패키지가 다르지 않을까 싶습니다!
이 어노테이션의 패키지가 2개 있는데요! javax.transaction.Transactional
이 존재하고 org.springframework.transaction.annotation.Transactional
이 존재해요!! 스프링에서 활용하는 트랜잭션은 후자로 혹시 패키지가 다르지 않은지 확인해주시면 좋을 것 같습니다!! 👍
만약 이 원인이 맞다면 패키지가 달라, Jdbi에서 트랜잭션이 적용되지 않은 이유인가 싶기도 하네요!! (물론 Jdbi를 지원하지 않을 수도 있습니다!! JdbcTemplate 지원까지는 확인했습니다!!)
패키지 변경으로 해결되신다면, build.gradle에 추가하신 bootJar은 제거해주셔도 될 것 같고, invalid source release는 아래 방법으로 시도해보시면 좋을 것 같습니다!! (java 17에서 비슷한 오류가 나온 사례인데, 해결 방법은 아마 동일할겁니다!! ㅎㅎㅎ https://binux.tistory.com/92)
[6. Jdbi 그리고 JPA]
제가 Jdbi를 사용해보지 않아 섣불리 말씀드리기는 어려울 것 같아요!! 다만, Jdbi가 ORM이 아니고 JdbcTemplate과 유사하고 조금 발전된 라이브러리라고 생각해 보았을 때는 위에서 말씀드린 SQL을 그대로 사용했을 때의 단점을 그대로 가지고 있는 것 같습니다!!
한국에서 느끼는 기술의 사용도는...! 자체 프로덕트를 가지고 있는 서비스 업계에서는 ORM 기술인 JPA의 활용도가 매우 높고, SI업계에서는 아직까지 MyBatis나 iBatis를 사용하고 있되, JPA를 점점 사용하려고 하는 추세인 것 같아요!
물론 제 주관적인 관점에서 느낀점이다 보니 실제와는 다를 수 있습니다 ㅎㅎㅎㅎ 😅
[마무리]
아이고~~~ 이렇게 정성스럽게 좋은 질문 가득 남겨주셔서 감사드립니다!! 질문을 많이 주시면 그만큼 Suyeon님께서 깊이 있게 봐주셨다는 의미가 되니 저야 감사드리죠~~ 🙇🙇
혹시나 위의 답변으로 충분하지 않은 내용이 있으시거나, 다른 부분 또 들으시다 궁금한 내용이 생기신다면 정말 편하게~ 질문 남겨주세요!!
Suyeon님께서도 새해 복 많이 받으시고, 올 한해 원하시는 바 모두 이루셨으면 좋겠습니다! 좋게 봐주셔서 감사합니다~!! 행복한 한 주 되세요~!! 🙏🙏
정성껏 답변해주셔서 너무 감사합니다! ReadOnly 빨간줄 에러 관련해서는 말씀해주신 import 부분이 맞았어요. 그런데 제가 다른 세팅을 다 확인해도 다운받은 준비 코드랑 완성된 코드 다 돌려보면 잘 빌드 되는데 제 코드는 계속해서 BootJar 에러가 뜨네요. Src 제 코드 안에 놓친 부분이 있는 건지 아니면 .idea(흰색 valid한 색이 아니라서) 에 문제가 있는 것 같아요. 시간이 너무 허비되서 이제 그만 들여다보고 그냥 완성된 코드 다운받아서 사용해야할 것 같아요 ㅠㅠ 추가로 몇 가지 질문이 더 있는데 다른 포스트에 내용 적겠습니다. 감사합니다!
그리고 자바홈을 adoptopenjdk-11.jdk로 설정하고 다시 테스트를 돌렸더니 모든 테스트가 fail이네요.. 아래 두 exception말고도 수십가지의 다른 springframework 에러들이 갑자기 뜨네요 ㅠㅠ
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException
Caused by: org.springframework.beans.factory.BeanCreationException
그러고 LibraryAppApplication을 run했는데
/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java...
Error: Main method is not static in class com.group.libraryapp.LibraryAppApplication, please define the main method as: public static void main(String[] args)라고 다시 자바로 되돌아가라는 메시지가 뜨네요 ;;;
제가 쓰는 jdk가 맞지 않는 걸까요 11인데도..