블로그

[냥이와봄] 7주차 (21.08.23 ~) - 첫 회고

✍ 6주차 돌아보기 월: 전원완료 화: 전원완료 수: 전원완료 목: 열공님 보류(사유: 병원) 금: 열공님 보류(사유: 병원) 토: kkm님 절반수강, 집가고싶다님 일욜보강           🙆‍♀️ 스터디 멤버 (총 8명) 냥집사, 집가고싶다, kkm, 이대건, 커피볶는정콩, 신규: 열공, boss, 옐로우티거        📖 7주차 커리큘럼 ( 스프링 핵심 원리 - 기본편, 스프링 MVC1편 - 백엔드 웹 개발 핵심 기술 ) 총 9강 월 : 섹션9. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결 화 : 섹션9. 웹 스코프 수 : 섹션9. request 스코프 예제 만들기 목 : 섹션9. 스코프와 Provider 금 : 섹션9. 스코프와 프록시 토 : 섹션10. 다음으로       스프링 MVC 1편 섹션1. 웹 서버, 웹 어플리케이션 서버       스프링 MVC 1편 섹션1. 서블릿       스프링 MVC 1편 섹션1. 동시요청 - 멀티쓰레드          🎉 짝짝짝~ [스프링 핵심 원리 - 기본편] 1회 수강 kkm님, 집가고싶다님, 커피볶는정콩님, 냥집사님, 이대건님 축하합니다. 나중에 2회 수강으로 뵙겠습니다. 😏  다섯분은 스터디 첫 느낌을 잊지 않도록 댓글에 짧게 1회 회고를 작성해주세요.    💡 스터디원 진행 사항 기존 스터디원 kkm - 커리큘럼과 동일 (강사 - 김영한님) 커피볶는정콩 - 커리큘럼과 동일 (강사 - 김영한님) 집가고싶다 - 커리큘럼과 동일 (강사 - 김영한님) 이대건 - 스프링 MVC 2편 (강사 - 김영한님) (스터디 들어오실때부터 이미 한번씩 다 들으신분👍) 냥집사 - 스프링 MVC 1편 중반 (강사 - 김영한님)   신규 스터디원 열공 -  스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (강사 - 김영한님) 옐로우티거 - 스프링 핵심 원리 (강사 - 김영한님) boss - 스프링 부트 개념과 활용 (강사 - 백기선님)    

웹 개발김영한#spring

JDK 동적 프록시 예제를 프록시 체이닝으로 구현...?

들어가기 전에김영한님의 스프링 핵심 원리 - 고급편 수업을 듣고 있던 중에 LogTraceBasicHandler에 필터링을 추가한다는 말을 듣고 이전 강의에서 말씀해주셨던 프록시 체이닝이 생각이 나서 "FilterHandler 이후에 LogTraceHandler 로 이어지는 프록시 체이닝을 보여주시려나" 보다 하고 있는데 그 둘을 합친 LogTraceFilterHandler을 생성하셔서 구현하시길래 "어라, JDK 동적 프록시는 프록시 체이닝으로 하기 까다로운가?" 라는 생각이 들었습니다.그렇다면 구현해보면 알 것 같았기 때문에, 한 번 저의 식으로 구현을 해보았습니다. 그리고 아래의 내용은 스프링 AOP와 CGLIB의 진도를 나가기 전에 작성되었습니다.목표강의 예제의 구조는 다음과 같습니다./** * JDK 동적 프록시 사용<br> * - {@link InvocationHandler} JDK 동적 프록시에 로직을 적용하기 위한 Handler<br> * - {@link PatternMatchUtils#simpleMatch}로 WhiteList 기반 URL 패턴 필터링 */ @Slf4j @RequiredArgsConstructor public class LogTraceFilterHandler implements InvocationHandler { private final Object target; private final LogTrace logTrace; private final String[] patterns; @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { // patterns 에 해당 메서드 이름이 없다면, 바로 목표로 이동 if (!PatternMatchUtils.simpleMatch(patterns, method.getName())) { return method.invoke(target, args); } // LogTrace 로직 실행 TraceStatus status = null; try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; status = logTrace.begin(message); Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } } 보면, 필터를 수행하는 로직과 로깅을 수행하는 로직이 하나의 메서드로 합쳐져 있습니다. 물론, 저 두개의 로직을 메서드로 따로 빼면 될 일이긴 합니다만… 저의 SRP 영혼이 슬프게 울고 있더군요.그리고 이전 수업 내용에 프록시의 장점 중 프록시 체이닝이란 것도 있기도 했고, Spring의 Filter도 이런식으로 구현되어 있을거니 나눠봤습니다.제가 구현하고자하는 내용은 아래와 같습니다.흐름만 봐서는 그냥 위의 코드와 동일하지만, 중요한 점은 필터를 담당하는 객체와 로깅을 담당하는 객체가 분리되었다는 것입니다. 그럼 구현을 한 번 해보죠.구현FilterHandler/** * 타겟의 메서드 이름을 필터링하는 Handler<br> * {@link PatternMatchUtils#simpleMatch}를 이용하여 패턴 검증<br> * - 해당 메서드의 이름이 패턴과 일치한다면: {@link #nextHandler}<br> * - 해당 메서드의 이름이 패턴과 일치하지 않는다면: {@link #target} * * @author MinyShrimp * @see Proxy * @see InvocationHandler * @see PatternMatchUtils#simpleMatch(String[], String) * @since 2023-03-02 */ public class FilterHandler implements InvocationHandler { private final Object target; private final Object nextHandler; private final String[] methodPatterns; /** * @param target 최종 목표 구현체 * @param nextHandler 다음 ProxyHandler * @param methodPatterns 필터링을 원하는 패턴 목록 - {@link PatternMatchUtils#simpleMatch} */ public FilterHandler( Object target, Object nextHandler, String[] methodPatterns ) { this.target = target; this.nextHandler = nextHandler; this.methodPatterns = methodPatterns; } @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { Object next = PatternMatchUtils.simpleMatch(methodPatterns, method.getName()) ? nextHandler : target; return method.invoke(next, args); } } 필터를 담당하는 프록시 핸들러입니다. 이 핸들러는 단순히 입력받은 패턴을 조사해 맞으면 다음 핸들러로, 맞지 않으면 바로 목표 구현체로 이동되도록 구현되었습니다.생성자를 보시면 알 수 있겠지만, 최종 목표 구현체(여기서는 OrderControllerV1Impl), 다음 프록시 핸들러(여기서는 LogTraceHandler), 그리고 패턴 패칭을 원하는 문자열 배열을 받습니다.이 예제와는 상관없지만, 개인적으로는 편의를 위해 생성자를 하나 더 만들어서 methodPattern이 배열이 아닌 하나의 문자열만 받을 수 있도록 구현해도 괜찮다고 생각이 듭니다. 하지만, 당장은 사용하지 않기 때문에 제거를 했습니다.그리고 final 맴버변수를 받기 위해 @RequiredArgsConstructor를 사용해도 괜찮지만, 그렇게 하게되면 위와 같이 주석을 남길 수 없기 때문에 사용하지 않았습니다.LogTraceHandler/** * Logging Handler<br> * {@link LogTrace}를 이용하여 로그 출력 * * @author MinyShrimp * @see Proxy * @see InvocationHandler * @see LogTrace * @see ThreadLocalLogTrace * @since 2023-03-02 */ public class LogTraceHandler implements InvocationHandler { private final Object target; private final LogTrace logTrace; /** * @param target 목표 구현체, 다음 ProxyHandler * @param logTrace {@link LogTrace} 구현체 */ public LogTraceHandler( Object target, LogTrace logTrace ) { this.target = target; this.logTrace = logTrace; } @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { TraceStatus status = null; try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; status = logTrace.begin(message); Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { logTrace.exception(status, e); throw e; } } } 로깅을 담당하는 프록시 핸들러입니다. 이 핸들러는 기존 강의에서 사용된 LogTrace를 받아 로그 메시지를 출력하는 역할을 수행합니다. 강의에서 제작한 LogTraceBasicHandler와 동일하기 때문에 설명을 생략합니다.다만, 정말 개인적인 아쉬움이긴 합니다만, 위의 TraceStatus를 받아옴에 있어서 단순히 저 Exception 하나 때문에 status 변수를 try 외부에 null로 선언하고 재할당 해주는 부분이 너무 아쉬웠습니다. 이를 해결하기 위해선 몇가지 방법이 있긴 합니다만, 이전 시간에 배운 ThreadLocal로 한 번 바꿔보겠습니다. ( 단순히 멤버 변수로 할당하면 동시성 문제가 발생합니다. )public class LogTraceHandler implements InvocationHandler { // 동시성 문제를 해결하기 위해 ThreadLocal 사용 private final ThreadLocal<TraceStatus> thStatus = new ThreadLocal<>(); // 중간 부분 생략 @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { try { String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; TraceStatus status = logTrace.begin(message); // 여기서 생성 thStatus.set(status); // ThreadLocal에 저장 Object result = method.invoke(target, args); logTrace.end(status); return result; } catch (Exception e) { TraceStatus status = thStatus.get(); // ThreadLocal에서 값을 가져옴 logTrace.exception(status, e); throw e; } finally { if (logTrace.isFirstLevel()) { thStatus.remove(); // 사용을 마치면 제거하자. } } } } 재할당하는 부분이 사라졌습니다! 이제 status를 받아오기 위해 ThreadLocal.get()을 사용하면 됩니다.그런데 여기서 주의사항이 있습니다. ThreadLocal의 사용이 끝나면 반드시 remove를 통해 지워줘야 합니다. 그래서 위와 같이 finally를 이용해 TraceId의 Level이 0인지 확인하고 0이면 remove를 하도록 작성해보았습니다. TraceId는 LogTrace가 가지고 있으니 넘겨주면 되겠군요.public class ThreadLocalLogTrace implements LogTrace { // 중간 생략 // LogTrace 인터페이스에도 추가해줍니다. @Override public boolean isFirstLevel() { return traceIdHolder.get().isFirstLevel(); // 그대로 넘겨줍니다. } } 좋습니다. 해결이 된 것 같군요. …과연 그럴까요? 이것 또한 버그가 있습니다.TraceId 는 ThreadLocalLogTrace에서 ThreadLocal로 잡고 있는 값입니다. 이것 또한 우리는 이전 시간에서 remove를 해주었습니다.public class ThreadLocalLogTrace implements LogTrace { // 중간 생략 /** * 이전 TraceID 로 전환<br> * - {@link #complete}에서 호출 */ private void releaseTraceId() { TraceId traceId = traceIdHolder.get(); if (traceId.isFirstLevel()) { traceIdHolder.remove(); // TraceId의 ThreadLocal을 제거한다. } else { traceIdHolder.set(traceId.createPreviousId()); } } } 겉보기에는 문제가 없어보입니다. 맞습니다. 평소에는 문제가 없습니다. 그런데 FirstLevel이 0이 되었을 때 문제가 발생합니다. 찬찬히 살펴보죠.위의 코드에서는 TraceStatus를 제거할 때는 finally에서 진행되고, TraceId를 제거할 때는 releaseTraceId에서 제거됩니다. 그리고 이 releaseTraceId는 end()와 exception()메서드에서 실행됩니다!즉, TraceStatus를 제거하기 위해 isFirstLevel() 메서드에서 traceIdHolder.get()을 사용하면, null을 리턴합니다. 그리고, null.isFirstLevel()은 NPE를 발생시킵니다. 그래서 아래와 같이 수정되어야 합니다.@Override public boolean isFirstLevel() { // releaseTraceId에서 제거되었다면 TraceId의 Level도 0이라는 소리. return traceIdHolder.get() == null; } DynamicProxyConfig/** * JDK 동적 {@link Proxy}를 스프링 빈으로 등록하기 위한 설정 파일 * * @author MinyShrimp * @see Proxy * @see FilterHandler * @see LogTraceHandler * @since 2023-03-02 */ @Configuration public class DynamicProxyConfig { /** * {@link FilterHandler}에서 사용하는 필터링 조건들, Whitelist 방식. */ private static final String[] METHOD_PATTERNS = { "request*", "order*", "save*" }; /** * @param target 최종 목표 구현체, 예) {@link OrderControllerV1Impl} * @param logTrace {@link LogTrace} * @return {@link FilterHandler} -> {@link LogTraceHandler} -> {@link OrderControllerV1Impl} */ private static Object filterLogProxyFactory( Object target, LogTrace logTrace ) { // 타겟이 상속받은 인터페이스들 중 첫 번째를 가져온다. // 해당 예제의 목표 타겟인 app.v1 들의 구현체들은 모두 인터페이스를 하나만 가지고 있기 때문에 가능하다. Class<?> superIntf = target.getClass().getInterfaces()[0]; // LogTraceProxy 생성 Object logTraceProxy = Proxy.newProxyInstance( superIntf.getClassLoader(), new Class[]{superIntf}, new LogTraceHandler(target, logTrace) ); // LogTraceProxy, 목표 타겟을 담은 FilterProxy 생성 return Proxy.newProxyInstance( superIntf.getClassLoader(), new Class[]{superIntf}, new FilterHandler(target, logTraceProxy, METHOD_PATTERNS) ); } /** * @return {@link OrderControllerV1Impl}의 Proxy */ @Bean OrderControllerV1 orderControllerV1(LogTrace logTrace) { OrderControllerV1Impl target = new OrderControllerV1Impl(orderServiceV1(logTrace)); return (OrderControllerV1) filterLogProxyFactory(target, logTrace); } /** * @return {@link OrderServiceV1Impl}의 Proxy */ @Bean OrderServiceV1 orderServiceV1(LogTrace logTrace) { OrderServiceV1 target = new OrderServiceV1Impl(orderRepositoryV1(logTrace)); return (OrderServiceV1) filterLogProxyFactory(target, logTrace); } /** * @return {@link OrderRepositoryV1Impl}의 Proxy */ @Bean OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) { OrderRepositoryV1Impl target = new OrderRepositoryV1Impl(); return (OrderRepositoryV1) filterLogProxyFactory(target, logTrace); } } 위에서 제작한 FilterHandler와 LogTraceHandler를 스프링 빈으로 등록하기 위한 설정 클래스입니다. 기본 베이스는 기존 강의에서 제작한 DynamicProxyFilterConfig와 동일합니다. 차이점이 있다면 각 빈에서 프록시 핸들러를 반환할때 발생하던 중복 코드를 filterLogProxyFactory() 메서드로 합쳐준 것 뿐입니다.그렇기 때문에 가장 중요한 filterLogProxyFactory()에 대해 설명하겠습니다./** * 이 메서드를 사용하기 위해선 FilterHandler와 LogTraceHandler는 * target의 첫 번째 인터페이스를 기반으로 프록시를 생성할 수 있어야 한다. * * @param target 1. 최종 목표 구현체, 예) {@link OrderControllerV1Impl} * @param logTrace {@link LogTrace} * @return {@link FilterHandler} -> {@link LogTraceHandler} -> {@link OrderControllerV1Impl} */ private static Object filterLogProxyFactory( Object target, LogTrace logTrace ) { // 2. 타겟이 상속받은 인터페이스들을 가져온다. Class<?>[] supIntfs = target.getClass().getInterfaces(); // 3. LogTraceProxy 생성 Object logTraceProxy = Proxy.newProxyInstance( supIntfs[0].getClassLoader(), supIntfs, new LogTraceHandler(target, logTrace) ); // 4. LogTraceProxy, 목표 타겟을 담은 FilterProxy 생성 return Proxy.newProxyInstance( supIntfs[0].getClassLoader(), supIntfs, new FilterHandler(target, logTraceProxy, METHOD_PATTERNS) ); } 이 메서드의 과정은 다음과 같습니다.타겟 객체(OrderControllerV1Impl)를 파라미터로 받아온다.타겟이 상속받은 인터페이스들의 정보(supIntfs)를 가져온다.그 정보를 이용해 LogTraceHandler의 프록시(logTraceProxy)를 생성한다.FilterHandler의 프록시를 생성하고 반환한다.기존 코드와 다른 점은 newProxyInstance()를 사용할 때, ClassLoader를 타겟의 첫 번째 인터페이스로 가져오는 것과, 두 번째 인자에 타겟의 모든 인터페이스들을 넣어 주는 부분입니다.이렇게 한 이유는 다름이 아니라, 이 메서드를 사용하는 모든 타겟 객체가 하나의 인터페이스만 상속받으며, 그 인터페이스를 이용해 핸들러들을 프록시로 만들어도 문제가 없기 때문입니다. 만약, 여러 개의 인터페이스를 상속받으며, 첫 번째 인터페이스를 기반으로 프록시를 생성하면 안되는 경우에는 위의 메서드를 사용할 수 없습니다. 그 때는 파라미터를 하나 추가해서 ClassLoader를 받아오면 됩니다.추가로 주의할 점은 FilterHandler 생성자에 LogTraceHandler를 주입한 것이 아닌, LogTraceProxy를 주입했다는 점입니다. LogTraceHandler도 invoke 함수가 제공되지만, 이는 Method의 invoke 함수와 무관하기 때문에 작동되지 않습니다. (예외가 발생합니다.)참고로, MainApplication에 해당 설정 파일을 등록하는 부분은 생략했습니다.완성…?이 구조를 완성했습니다. 구조 자체는 간단합니다만, 이쁘게 만들려다보니 신경써야할 부분이 좀 많았습니다.물론, 처음에 코드를 작성할 때 TraceStatus를 멤버 변수로 등록했다가 동시성 문제도 터지고, 위에서 설명한 finally에서도 NPE가 발생하기도 하고, FilterHandler에 LogTraceProxy를 넣어야하는데 LogTraceHandler를 넣어서 체이닝이 안되기도 했습니다만 구현해 놓고 보니 뿌듯합니다.하지만 아직 미심쩍은 부분과 아쉬운 부분이 보입니다.“ThreadLocal가 좋은건 알겠는데 이렇게 많이 사용해도 괜찮나…?”ThreadLocal의 가장 큰 주의점은 쓰레드 풀 환경에서 remove를 해주지 않는다면 다른 유저가 그 정보를 볼 수 있다는 점입니다. 그리고 ThreadLocalMap은 크기가 커지면 커질 수록 2배의 크기로 할당한다는 점입니다. 지금까지는 별 문제가 없어보이지만, 다른 프로그래머가 코드를 수정하여 remove가 실행이 안되게 되거나, 비즈니스 로직 변경으로 인해 급하게 수정하다가 remove를 놓치게 되면 위의 문제는 꽤 심각하게 발생합니다. 이때는 어떻게 대처를 해야하나요? 아니면 위 문제는 별로 신경쓰지 않아도 되나요?“지금처럼 핏한 상황은 잘 작동하지만, 이 코드를 확장하려면 고칠 부분이 많네…”꽤 만족할만한 코드 퀄리티라고 생각은 합니다만, 위의 코드를 재사용하여 확장해야 하는 상황이 온다면 고쳐야할 부분이 눈에 들어옵니다. 일례로, 목표 타겟의 인터페이스 상속이 늘어나고 순서가 바뀌면 위의 코드의 filterLogProxyFactory는 더이상 사용할 수 없습니다.그리고 Handler가 늘어나면 설정 파일에서 그에 맞게 주입을 먼저 해주어야합니다. 이는 전략 패턴의 단점과도 연결됩니다. 또한, FilterHandler 와 같이 분기점에 따른 Proxy 변경도 많아지게 되면 일반화를 진행해야합니다.(BranchHandlerFactory 와 비슷한 이름으로..)정리사실, 위의 로직들은 JDK 동적 프록시가 아닌 다른 방법으로 구현하는게 맞습니다. 스프링 MVC에서 배운 필터와 인터셉터도 있고, (저는 아직 진도를 안나갔습니다 만은)앞으로 배울 스프링 AOP가 해결 방법이 될 수도 있습니다. 그럼에도 이렇게 시간을 들여 글을 쓰는 이유는 다음과 같습니다.코드를 구현할 때 어떤 방식으로 구현하는지 생각을 정리하고 기록을 남기기 위함이었습니다.혼자서 코드를 작성하는 것은 언제나 자신과의 싸움을 하고 있다는 말과 같습니다. 보통 이런 상황에서 누군가에게 피드백을 받기란 요원한게 사실입니다. 그리고 그 기간이 길어지면 길어질수록 현재 자신의 위치가 어느 정도인지 짐작조차 할 수 없게 되고 다른 사람에게 물어볼때 어떤 방법으로 물어봐야하는지 모를 수 밖에 없습니다.“지금 짜고 있는 코드가 좋은 코드인가?” 과연 어떤 프로그래머가 이 생각을 하지 않겠냐만은, 혼자서 코드를 작성하면 정말 나쁜 코드를 작성하더라도 위에 대한 판단을 내릴 수가 없습니다. 또한, 나쁜 코드를 벗어나서 좋은 코드로 향하는 방법을 알고 싶어도 키워드를 모르니 방법이 없는 거지요.그래서 저의 코드 구현 방식을 공유하고 다른 분들에게 피드백을 받기 위해 이 글을 작성했습니다.현재 구현한 이 방법보다 더 좋은 방법이 무엇인지, 어떤 사이트 이펙트가 발생하는지, 내가 생각하고 있는 개념이 맞는지, JavaDoc 쓰는 방법은 올바른지, 등등 알고 싶은게 많습니다. (그러니까 비-법 소스 주세요!!!)꼭 긴글이 아니더라도 지나가는 말처럼 짧은 키워드만 툭툭 던져주셔도 저같이 공부하시는 분들에게는 많은 도움이 됩니다. 긴 글 읽어주셔서 감사드리며, 저와 같은 다른 취준생 여러분들도 다 같이 화이팅입니다. ^^7

백엔드JavaSpring김영한스프링핵심원리고급편JDK동적프록시프록시체이닝공유

자바 ORM 표준 JPA 프로그래밍 기본편(김영한) 2

프로젝션 JPQL의 경우 패키지명까지 참조해서 생성자 매핑시켜주기 ex) select com.myproject.myapp.dto.MemberDto(m.id, m.name) ... result class 또한 DTO로 매핑 querydsl의 경우 @QueryProjection을 통해 querydsl용 생성자를 만들어서 매핑 가능 이 경우, select절에 Projections.constructor(Dto.class, ...)로 매핑 Projections.bean과 Projections.field에 비해 연쇄 수정이 적어지는 장점이 있음   페치 조인 페치 조인은 연관관계 엔티티까지 한 번에 불러오는 것 일대일 연관관계에서 주로 사용 일대다 컬렉션 페치 조인은 한 번만 지원되며,사용 시 중복조회가 일어나기 때문에 select distinct 사용하여야함 @BatchSize() 또는 hibernate.default_batch_fetch_size 사용해서 fetch join으로 불러올 엔티티 개수 지정 가능 원하는 필드만 조회하거나 연관관계가 조건절에만 필요한 경우 fetch join이 아닌 일반 조인 사용할 것.   다형성 쿼리 상속 연관관계 SINGLE_TABLE 타입의 경우type(i) in (Book, Movie) 또는 treat(i as Book).author = 'kim' 으로 사용   정적 재사용 쿼리 @SqlResultSetMapping(name = "NamedQuery 이름",entities = {    @EntityResult(entityClass=Member.class,                                   fields = { @FieldResult(name = 'id', column = 'order_id') }                                   column이 실제 테이블 컬럼명, 타입도 지정 가능                                  )           },columns = {@ColumnResult(name = 'item_name')}이것을 통해 projection 필드(가공한 필드)를 가져올 수 있음) NamedQuery 재사용 가능한 정적 쿼리 - 파라미터도 전달 가능 어플리케이션 로딩 시점에 쿼리 검증함 @Query도 NamedQuery @NamedQuery(name = "", query = "") NamedNativeQuery name, query, resultClass   벌크연산 변경감지만으로 데이터 수정하면 업데이트 쿼리가 남발됨. update, delete, insert select 사용 execute하면 영향받은 엔티티수 반환 벌크 연산은 DB에 직접 쿼리 날리기 때문에 영속성 컨텍스트 초기화가 필요@Modifying 또는 em.flush(); em.clear();        

JPA강의김영한프로젝션페치조인다형성쿼리Named쿼리벌크연산

spring mvc 2(김영한) - 메세지 소스, 검증, 로그인

메세지 소스 변수 사용 가능, 점(.)으로 이름 나눌 수 있음, default message 제공 가능   검증 bindingResult -> addError() rejectValue() FieldError() 파라미터 값도 재전달 가능 bindingResult에서도 message source 사용 가능 spring.messages.basename=messages,errors -> errors.properties에서 사용 단계적 메세지 처리 가능; 자세한 값, 기본값 순으로 메세지 반환함 code.objectName.fieldName 식으로 값 설정하면됨(typeMismatch.user.age) implements Validator, @Override supports(Class<?> clazz), @Override validate(Object target, Error errors)   검증2 - Bean Validation @NotNull @Email @Range(min, max) @Max(int) @Pattern(regexp="") 상황에 따라 제한조건이 다름 - null 또는 not null 이 경우, @NotNull(groups={SaveCheck.class, UpdateCheck.class}) 설정 후 @Validated(SaveCheck.class)로 사용하거나 dto 분리해서 사용하면 됨   로그인 처리 Cookie or @CookieValue() 초기화 시 null값 넣어주고, setMaxAge(0) (public static final == interface 일반 변수) HttpSession or @SessionAttribute   필터 웹과 관련된 공통관심사는 AOP보다 서블릿 필터 또는 인터셉터로 해결하기 Http 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 Filter 사용하고 chain.doFilter()해줘야 그 다음 단계로 넘어감 FilterRegistrationBean<Filter> setFilter() addUrlPatterns() 필터 조건에 맞지 않는 경우 거를 때는 sendRedirect로.   인터셉터 서블릿 필터는 서블릿이 제공, 스프링 인터셉터는 스프링이 제공 Http 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 HandlerInterceptor, preHandle(), postHandle(), afterCompletion() ModelAndView 사용 가능 webConfig 빈등록 addInterceptors(InterceptorRegistry registry) registry.addInterceptor(new CustomInterceptor()) addPathPatterns("/**"), excludePathPatterns("/css/**", )    

강의김영한

spring mvc 2(김영한) - 오류처리, 컨버터, 파일업로드

오류 처리 response.sendError(404, "404오류"); extends webServerFactoryCustomizer<ConfigurableWebServerFactory> void customize(ConfigurableWebServerFactory factory) new ErrorPage(status, path) factory.addErrorPages(); 예외 발생 시 WAS로 다시 가서 필터 서블릿 인터셉터 컨트롤러를 재호출한다.   필터 오류 처리 add DispatcherType ERROR(서버 오류 반환) 기본값은 REQUEST(클라이언트 요청)   인터셉터 오류 처리 excludePathPatterns 에서 error 페이지 경로 추가하기 오류 발생 시, after completion 호출 후 에러 페이지로 이동 추가 안 한다면, post handler 거쳐서 이동   스프링 기본 에러 반환 return /error 경로 지정하면 생성한 오류 페이지 자동 반환 우선순위는 templates(500, 5xx) -> static(500, 5xx) -> templates(error.html) API 에러 처리는 @RequestMapping에 produces = MediaType JSON으로 처리 BasicErrorController에서는 produces 자동 구현되어있음(/error)   HandlerExceptionResolver implements HandlerExceptionResolver e instanceOf IllegalArgumentException response.sendError() || ModelAndView || response.getWriter().println() 으로 json 반환도 가능 WAS로 안 돌아감 webConfig에 extendHandlerExceptionResolvers 빈등록 필요 CustomException에 @ResponseStatus 지정 가능, reason 속성으로 메세지 지정도 가능return ResponseStatusException으로 바로 반환도 가능   컨버터 implements Converter interface <S, T> @Override convert() ConversionService DefaultConversionService, addConverter(new CustomConverter()) 등록 후, 서비스 의존주입 받아서 service.convert("10", Integer.class) 뷰 템플릿에서는 ${{}} 또는 th:field로 컨버터 적용가능   Formatter Formatter<Number> 확장받아서 parse 메서드 오버라이드; NumberFormat.getInstance(locale), return numberFormat.parse(변수) print 메서드 오버라이드 numberFormat.format(변수) 컨버전 서비스 등록 시 포맷터 추가 가능(addFormatter()) 스프링 기본 제공 포맷터@NumberFormat, @DateTimeFormat 등   파일 파일업로드 multipart/form-data 타입으로 전송 서로 다른 타입의 테이터를 한 번의 form 전송으로 전송 spring.servlet.multipart.max-file-size 또는 max-request-size 설정 file.dir 파일 저장할 주소를 변수로 생성한 후 @Value("${file.dir}")로 가져와서 사용가능 MultiPart(Part) -> part.write(fileDir + part.getSubmittedFileName()); MultipartFile로 파라미터 받고 !file.isEmpty()일 경우에 file.transferTo(new File(fileDir + file.getOriginalFilename())) 업로드 시 확장자 분리 -> originalFilename.substring(lastIndexOf(".") + 1)새로운 파일 저장경로에 확장자 붙여서 저장   첨부 이미지 다운로드 @ResponseBody로 반환 + return new UrlResource("file:" + file~.getFullPath(filename)); 첨부파일 다운로드 시에는 UrlResource + header 설정해주고 넘겨줘야함 contentDisponsition = "attachment; filename=\"" + UriUtils.encode(uploadFileName, StandardCharsets.UTF-8) + "\"" .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)    

김영한강의

스프링 핵심원리 기본편(김영한) 1 - 객체지향 DIP와 스프링 DI, IoC

  객체는 객체와 끊임없이 상호작용한다. 그렇기에 유연한 변경이 가능해야한다. 예를 들어, 자동차라는 상위 클래스를 다양한 자동차 브랜드로 구현될 수 있고, 운전자가 변화해도 자동차는 영향을 받지 않는다. 사용자, 주문, 할인 등 여러 독립적인 특징을 가진 기능은 클래스로 분리하여 각 클래스에서만 수정 및 사용한다.   역할과 구현을 분리 - 인터페이스와 콘크리트 클래스 인터페이스는 안정적이게, 확장이 무한대로 가능하게 설계해야한다.   SOLID 객체지향 설계 원칙 1. SRP 단일책임원칙 - 변경이 용이한 단위적 책임인가2. OCP 개방폐쇄원칙 - 코드의 변경 없이 확장이 가능한가(조립만으로 변경)3. LSP 리스코프 치환 원칙 - 하위 클래스는 인터페이스(상위 클래스)를 위반하지 않아야한다4. ISP 인터페이스 분리 원칙 - 여러 개의 인터페이스를 통해 명확한 기능을 갖고 있고, 대체 가능성이 높은 환경을 구현할 것5. DIP 의존관계 역전 원칙 - 추상화에 의존할 것, 인터페이스(역할)가 중심이 되어야한다. 구현체에 의존하면 다형성을 잃는다(재활용성을 잃는다) 스프링 컨테이너에 객체 지향 적용 객체를 생성하는 역할과 객체를 실행하는 역할을 분리.의존은 인터페이스로 하고, 설정 파일을 통해 구체적인 구현체를 의존 주입구현체 변경 시 설정 파일만 변경하면 된다.(조립)=> 제어의 역전; 어떤 구현체를 사용할 것인지 AppConfig(Spring)가 결정한다. 동적인 인스턴스 의존관계    

객체지향javaSOLIDspringDIIoCDIP강의김영한

실전 Querydsl(김영한) 1

문법countcolumn.sum avg max minjoin, orderBy, groupBy, having()eq, in, isNull(), between, goefetch, fetchOne, fetchResults   조인join(innerJoin), leftJoin, fetchJoin연관관계 없는 세타 조인 from(member, team) - on절 사용 가능(leftJoin)세타조인이 leftJoin으로 진행될 경우 별칭사용 안 함   서브쿼리JPAExpressions - static으로 사용하기QMember memberSub = QMember("memberSub")로 엔티티 별칭으로 참조   기타assertThat(result).extracting("age").containsExactly(30)case문 - when().then().otherwise()   프로젝션 Projections.bean(MemberDto.class, member.username, member.age)setter 필요 Projections.field() Projections.constructor() DTO 생성자에 @QueryProjection 붙여서 사용as 또는 ExpressionUtils로 dto 필드명에 맞추기   동적쿼리BooleanBuilder builder if(arg != null)builder.add(member.username.eq(arg)) jpaQueryFactory.selectForm().where(builder) where 다중 파라미터 -> 쿼리 조립 재활용 가능- Predicate, BooleanExpressions 사용- null 주의   기타update().set(member.age, member.age.add(1))delete().where(member.age.gt(10))sql function 사용 가능 ex) DATE_FORMAT   조회 API 컨트롤러jpaRepository.search(condition)MemberSearchCondition dto @QueryProjections쿼리스트링만으로 쿼리 자동생성 & 검색 기능 사용 가능   사용자 정의 리포지토리CustomRepositoryImpl interface CustomRepository extends MemberRepository(== JpaRepository)   페이징offset limit fetchResults => list, count 쿼리 호출 -> deprecatedfetchCount와 fetch로 Page 생성 가능또는 직접 count 쿼리 + select list 쿼리 조합다음은 필요할 때만 count 쿼리 실행하는 코드PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount())

김영한강의Querydsl

자바 ORM 표준 JPA 프로그래밍 기본편(김영한) 1

JPA 정리 엔티티 매니저 엔티티 매니저 팩토리에서 엔티티 매니저들을 생성하며, 각 엔티티 매니저는 DB 커넥션 풀을 사용힌다. 엔티티 매니저를 통해서 영속성 컨텍스트 생성한다. 영속 상태는 커밋상태가 아니다. 커밋은 트랜잭션 단위이다.   영속성 컨텍스트의 특징 1차 캐시 동일성 보장 쓰기 지연(persist, flush) 변경 감지 지연 로딩(lazy)   연관관계 연관관계 주인을 판단하는 기준 : 외래키를 갖고있는가 또는 등록수정이 빈번한가 연관관계 주인에서는 컬럼에 @JoinColumn으로 조인 시 어떤 컬럼을 사용할 것인지 명시 가능하다. 연관관계 주인이 아닌 쪽에서는 mappedBy로 양방향 연관관계를 설정해줄 수 있다.양방향은 DBMS상에서는 없는 개념이지만 엔티티에서는 참조가능하다. 다대다 연관관계는 중간 테이블을 사용힌디/ - 일대다 & 다대일 테이블로 연결 cascade와 orphanRemovel를 통해 자식객체를 제어할 수 있다.부모엔티티에 있는 연관관계에서 사용 (@OneToOne 또는 @OneToMany) 프록시 객체로 조회하고, 초기화 요청들어가면(호출되면) 실제 객체에서 정보 추출캐시에 있다면 바로 실제 객체를 반환한다.   값 타입과 임베디드 타입 임베디드 타입은 객체 속성을 응집시킬 뿐만 아니라 도메인 메서드를 통해 개별적 처리가 가능하다.(객체지향적) 임베디드 타입을 한 엔티티에서 두 번 참조할 때 DB에 필드가 다르게 들어가야한다.-> 이 경우, AttributeOverride 어노테이션으로 컬럼명을 설정해줄 수 있다. 임베디드 타입은 같은 객체를 사용했을 때 객체 수정 시 모든 값이 변경될 수 있다.여러 엔티티에 공유하지 말고 각각 생성해야하며 수정 시에도 생성자를 통해 통으로 변경해야한다. 되도록이면 엔티티화(연관관계)하는 것이 식별자를 통해 추적, 변경할 수 있기 때문에 편리하다.   상속관계 @Inheritance(strategy=InheritanceType.XXX) 전략 JOINED 부모 자식 모두 테이블이 생성되며, 부모와 자식테이블은 부모의 pk로 연관관계가 이어져있다.  자식 insert 시 부모까지 insert되며, 조인을 통해서 테이블을 관리한다. SINGLE_TABLE 부모 테이블만 생성되며 자식 엔티티는 부모 테이블에 컬럼으로 추가된다. 부모클래스에 @DiscriminatorColumn(name="DTYPE; 컬럼 이름") 자식클래스에 @DiscriminatorValue("컬럼의 값으로 사용할 이름") PER_CLASS 자식 엔티티들만 독립적인 테이블을 생성하며 모두 부모 엔티티의 컬럼을 가진다. 자식 테이블의 pk는 부모 테이블의 pk를 사용한다.    

강의김영한JPAEntityManager영속성연관관계임베디드타입상속관계

실전! 스프링 부트와 JPA 활용2 (API 개발과 성능 최적화) - 김영한

 Rest API를 구현하며 알아보는 Entity를 반환하는 6가지 방식   Version 1. 엔티티를 직접 노출하는 방식 [문제] 엔티티를 직접 반환하는 방식은 중요한 정보가 노출되거나 필요하지 않은 데이터까지 불러 데이터 구조가 비대해질 수 있다.연관관계까지 호출할 경우, 역시 모든 속성을 호출함으로 비대해진다. 양방향 연관관계일 경우 한 쪽 컬럼에 @JsonIgnore을 설정하여 무한루프가 빠지지 않도록 주의해야한다.연관관계는 fetch option을 lazy로 설정하면 호출되지 않기 때문에 lazy로 설정한 후 필요한 경우 속성을 호출하는 방식으로 사용하는 것을 권장한다.   Version 2. 엔티티를 DTO로 변환하는 방식 stream을 이용한 entity -> dto 변환stream에서 filter, map을 사용해 중간변환을 하고 find 또는 collection 형태로 최종 가공. 필요한 속성 중심으로 가공할 수 있어 깔끔하게 전달 가능 [문제] 여전히 쿼리 반환 시 모든 데이터가 호출되기 때문에 성능 상 문제가 생길 수 있음   Version 3. 엔티티를 DTO로 변환하며 페치 조인으로 최적화 jpql에서는 join fetchquerydsl에서는 join() 후 fetchJoin() fetch join을 이용하여 한 번의 쿼리로 연관관계까지 모두 호출하도록 한다.그렇게 되면 지연 호출(lazy)로 인한 추후 연관관계 호출 시 추가적인 쿼리 발생이 일어나지 않으며,크로스 조인(cross join)으로 인한 데이터 중복 호출도 발생하지 않는다. 일대다 컬렉션 조인이 있을 경우, select distinct로 데이터 뻥튀기를 해결해줘야 한다. [문제] fetch join의 경우, 일대다 컬렉션 연관관계는 한 개 밖에 사용하지 못한다는 한계가 있다. 또한 컬렉션 페치 조인을 사용하면 페이징이 불가능하다.모든 데이터를 읽어온 후, 메모리에서 페이징을 한다;;   Version 3.1. 컬렉션 페치 조인 - 페이징 한계 돌파 일대다 관계에서 페이징을 하려면 일(1)을 기준으로 해야되지만컬렉션 페치 조인을 사용할 경우 다(N)이 기준이 되어 row가 생성된다.-> 하이버네이트는 이러한 문제로 메모리에서 페이징을 진행한다; [해결] ToOne 연관관계는 fetch join으로 호출한다. ToMany 컬렉션 연관관계는 지연로딩으로 조회한다. 이 때, hibernate.default_batch_fetch_size 또는 @BatchSize을 이용한다.이 옵션은 컬렉션 또는 프록시 객체를 설정한 size만큼 in query로 조회한다.1 + N 쿼리에서 1 + 1 쿼리로 최적화 된다.   Version 4. JPA에서 DTO 직접 조회 특정 용도에 필요한 컬럼들로만 구성한 DTO로 조회 jpql의 경우, select new package명.dto.ResponseDto(컬럼...) 및 클래스 명시querydsl의 경우, select(QueryProjections.constructor(Dto.class, 컬럼...)) 역시, 컬렉션 조회 시 1+N 문제가 있고, 해당 문제는 아래에서 다룸   Version 5. JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화 컬렉션 연관관계는 일(1)의 id 목록을 파라미터로 전달하여 in query 별도 조회stream groupingBy로 일(1)의 id로 다(N)를 묶은 후 -> 일(1)의 속성에 넣어줌 /** order과 order_item으로 살펴보는 예시 */ // 1. order 목록 호출 List<OrderQueryDto> result = findOrders(); // 2. order의 id 추출 List<Long> orderIds = result.stream() .map(o -> o.getOrderId()) .collect(Collectors.toList()); // 3. order id로 order item 호출 List<OrderItemQueryDto> orderItems = em.createQuery( "select new ...OrderItemQueryDto(컬럼...)" + " from OrderItem oi" + " join oi.item i" + // 참고로, fetch join은 엔티티 조회에서만 가능 " where oi.order.id in :orderIds", OrderItemQueryDto.class) .setParameter("orderIds", orderIds) .getResultList(); // 4. order item을 order id로 grouping Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream() .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId())); // 5. order에 order item 매핑해주기 result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));   Version 6. JPA에서 DTO로 직접 조회 - 플랫 데이터 최적화 쿼리 한 번으로 모든 데이터 조회 [문제] 조인으로 인해 중복 데이터 생성될 수 있다.쿼리에서는 데이터를 모두 호출하기 때문에 애플리케이션에서 추가 작업이 발생할 수 있다.페이징이 불가능하다.

강의김영한REST_APIDTO

채널톡 아이콘