왜 푸른수염이냐고요? 제목 재미없게 쓰면 안들어와볼거잖아요 ㅠ

 

르세라핌의 콘서트가 열렸다. 콘서트에 입장하는 사람들과 권한을 어떻게 관리할까?

보안 검색대와 VIP 팬클럽

콘서트장 입구에는 강력한 보안팀이 존재한다.

먼저, 모든 입장객에 대해서 철저히 확인될 필요가 있다. 신분증 확인, 티켓 검사, 가방 체크 등 모든 사람은 입장 전에 이 과정을 통과해서 어떤 사용자인지 판별이 되어야 입장이 가능하다. 이처럼 필터는 모든 HTTP 요청에 대해서 작동한다. 인증, 인코딩 설정, CORS 처리 등 모든 요청을 대상으로 한다. 기본적인 자격을, 입장하는 모든 사용자들을 대상으로 진행하는 것이다.

 

콘서트 내부에서는, VIP 팬클럽 전용 구역이 존재한다.

해당 구역에 들어가려면 팬클럽 가입 여부와 얼마나의 등급인지를 확인해야 한다. 이 판별기의 역할을 하는 것이 바로 인터셉터이다. 모든 관객이 아니라, 팬클럽에 가입된 멤버만을 대상으로 확인한다. 이는 특정 요청이나 특정 컨트롤러에만 작동하는 인터셉터와 같다.

먼저 전용 구역에 입장하기 전에 판별(preHandle)해서, 팬클럽 멤버의 QR 코드 등으로 멤버 여부를 확인하고, 멤버가 맞다면 입장 허가를 내리고, 아니라면 일반 구역으로 이동하게 한다.

다음으로, 팬클럽 멤버임이 확인(postHandle)되면, 특별 기념품을 제공하거나 특별 전용 구역으로 입장을 허용한다.

마지막으로 콘서트가 끝난(afterCompletion) 후, 팬클럽 멤버의 만족도를 조사하고 기념품 수령 여부를 확인한다.

 

코드로 알아보기

먼저 Filter를 구현해보자.

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*") // 모든 경로에 대해 필터 적용
public class SecurityFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필터 초기화 로직 (필요 시)
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("모든 관객: 티켓 검사 중...");
        // 요청을 다음 필터나 서블릿으로 전달
        chain.doFilter(request, response);
        System.out.println("모든 관객: 입장 확인 완료!");
    }

    @Override
    public void destroy() {
        // 필터 종료 로직 (필요 시)
    }
}

 

모든 로직은 필터를 반드시 거치게된다.

 

 

다음으로 인터셉터의 코드이다.

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class FanClubInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String memberId = request.getHeader("Fan-Club-ID"); // 헤더에 팬클럽 ID가 있다고 가정
        if (memberId != null && isValidFanClubMember(memberId)) {
            System.out.println("팬클럽 멤버 확인됨: 특별 혜택 제공 준비 중...");
            return true; // 컨트롤러로 요청 진행
        } else {
            System.out.println("팬클럽 미가입자: 일반 구역 입장으로 안내");
            response.sendRedirect("/general-zone");
            return false; // 요청 중단
        }
    }

    private boolean isValidFanClubMember(String memberId) {
        // 팬클럽 멤버 유효성 검사 로직 (데이터베이스 조회 등)
        return "12345".equals(memberId); // 예시로 ID가 12345인 경우만 팬클럽 멤버로 판별
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("팬클럽 멤버 혜택 준비 완료 후 추가 작업 수행 중...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("팬클럽 멤버의 공연 후 마무리 작업 중...");
    }
}

 

 

참고. 앞으로 설명하면서 나올 DispatcherServlet 에 대한 설명은 이 글 제일 하단에 설명해놓았다.

필터는 전통적인 웹구조, 즉 JSP/Servlet 기반의 구조에서 주로 사용된다.

즉, Servlet 컨테이너(ex. Tomcat)를 통해 클라이언트의 HTTP 요청을 처리하는 것이 주 목적이다. 

 

HTTP로 요청이 들어오면, 요청은 서블릿 컨테이너를 거쳐 서블릿이나 JSP로 전달된다. 서블릿이 요청을 처리하고 응답을 작성한다. 이때 응답은 다시 필터를 거쳐 클라이언트로 반환된다. 다음의 구조를 따르는 것이다.

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 // 콘서트에 입장 가능한 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) // 콘서트에 입장 불가능한 사용자

 

필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.

  • init(): 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
  • doFilter(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
  • destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.

 

인터셉터는 RESTful API의 클라이언트-서버 구조에서 주로 사용된다.

즉, JSON 또는 XML과 같은 형식으로 데이터를 교환하며, 무상태(stateless) 프로토콜을 따른다.

 

필터와 다르게, Spring MVC에서 주관하기 때문에, 서블릿 호출 다음에 호출된다. 다음과 같다.

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 // 입장 허용 관객
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출X) // 입장 불가 관객

 

서블릿 필터의 경우 단순히 request , response 만 제공했지만, 인터셉터는 어떤 컨트롤러( handler )가 호 출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있다.

  • preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.)
  • postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.) (컨트롤러에서 예외가 발생하면 호출되지 않는다.)
  • afterCompletion : 뷰가 렌더링 된 이후에 호출된다. (예외가 발생해도 항상 호출된다. 예외는 ex 파라미터로 받아서 어떤 예외가 발생했는지 로그로도 항상 출력할 수 있다. 예외가 발생하지 않았다면 ex 파라미터는 null이다.)

 

필터는 누가 관리하는가? : Servlet 컨테이너.

필터는 Java Servlet 스펙의 일부로, HttpServletRequest (http요청객체) 와 HttpServletResponse (http 응답객체) 를 다루는 Servlet 컨테이너 (ex. Tomcat, Jetty) 에 의해 실행된다.

즉, 웹 애플리케이션의 가장 앞단에서 작동하며, 요청이 DispatcherServlet이나 다른 서블릿에 도달하기 전에 실행된다. 필터는 서블릿 컨테이너가 http 요청을 받을 때 실행되며, 서블릿 컨테이너 레벨에서 http 요청과 응답을 가로챈다.

필터는 javax.servlet.Filter 인터페이스를 구현하여 사용할 수 있다.

 

인터셉터는 누가 관리하는가? : Spring MVC 라이브러리.

인터셉터는 Spring MVC 라이브러리에 의해 관리되고 실행된다. 이는 Spring의 DispatcherServlet이 요청을 처리할 때 실행되며, 컨트롤러로 요청이 전달되기 전(preHandle), 컨트롤러 처리 후(postHandle), 그리고 요청 완료 후(afterCompletion)에 동작한다.

Spring MVC의 컨텍스트 내에서 작동하므로, 요청이 DispatcherServlet에 의해 처리될 때만 실행된다.

org.springframework.web.servlet.HandlerInterceptor 인터페이스를 구현하여 사용할 수 있다.

 

 

DispatcherServlet?????

Spring Framework의 핵심 구성 요소 중 하나로, Spring MVC의 프론트 컨트롤러(Front Controller) 역할을 한다. 모든 HTTP 요청은 이 DispatcherServlet을 통해 들어오고, 이  DispatcherServlet이 요청을 적절한 핸들러(컨트롤러)로 전달하여 처리한다.

 

DispatcherServlet 의 HTTP 수문장으로써의 역할.

 

1. HTTP 요청 수신

2. 핸들러(컨트롤러) 매핑

3. 핸들러(컨트롤러) 실행

4. 뷰/응답 처리 및 반환

  • 페이지 반환의 경우 : ModelAndView 객체를 통해 어떤 뷰(View)를 렌더링할지 결정하고, ViewResolver를 사용해 뷰를 찾아서 반환한다.
  • RESTful API 의 경우 : 컨트롤러에서 반환된 데이터가 @ResponseBody 나 ResponseEntity를 통해 JSON 또는 XML 로 변환된다.(이때 메시지 컨버터(HttpMessageConverter)가 변환(직렬화)하는 역할을 한다.) 반환된 데이터를 응답 본문(data)에 포함시켜 클라이언트로 전송한다.

 

결론.

HTTP 요청을 처리하는 기본 웹 컴포넌트인 서블릿은, 필터와 인터셉터의 처리 이후에 비즈니스 로직을 수행한다.

필터는 웹 애플리케이션의 광범위한 요청/응답 처리에 사용되고, 인터셉터는 Spring MVC 컨트롤러 전후의 세밀한 처리를 위해 사용된다.

스프링에는 위장술을 좋아하는 여러 빈들이 살고 있다.

각 빈은 본인 모습 그대로, 즉 객체 그대로를 내보이고 싶어하지 않아한다. 그렇기 때문에 빈들은 위장술을 사용하게 되는데, 바로 프록시의 모습으로 위장하는 것이다.

 

어드바이저, 오늘은 어떤 스타일로 꾸며볼까?

빈은 하나의 프록시로 위장할지언정, 여러 스타일이 존재한다. 어드바이저에 추가하는 어드바이스만큼 꾸미기를 할 수 있는 것이다.

 

이때, 어드바이스란?

https://jinn-o.tistory.com/295

 

[Spring AOP] Advice, Spring이 동적 프록시를 추상화하는 방법

먼저, Java 에서 JDK 동적 프록시란?https://jinn-o.tistory.com/293 [JDK 동적 프록시] 프록시 객체들을 공통화 하기먼저 프록시(Proxy)란?프록시란, 주로 실제 객체 앞에서 실행되는 대리 객체처럼 이해하면

jinn-o.tistory.com

 

빈 후 처리기는 스프링이 실행될때, 지정한 빈에게 프록시를 일괄 적용해준다.

다음은 빈 후 처리기를 사용할 수 있는 인터페이스 규격이다.

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // 초기화 전 처리 로직
        System.out.println("초기화 전 처리: " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 초기화 후 처리 로직
        System.out.println("초기화 후 처리: " + beanName);
        return bean;
    }
}

 

BeanPostProcessor 를 사용해서 빈으로 등록하면, 각 빈들에게 일괄적으로 적용된다.

 

스프링부트는, 자동 설정으로 빈 후 처리기를 스프링 빈에 자동 등록한다.

다음 라이브러리를 추가하면, aspectjweaver 라는 aspectj 관련 라이브러리가 등록하고, 스프링부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.

    implementation 'org.springframework.boot:spring-boot-starter-aop'

gradle external library 에 추가되어 있는 모습

 

단순히 라이브러리를 추가하는 것 만으로, 자동 프록시 생성기가 만들어졌다.

이게 바로 스프링의 빈 후 처리기인, BeanPostProcessor 이다. 이름하여, AutoProxyCreater -!! 더 정확하게는, AnnotationAwareAspectJAutoProxyCreator 클래스이다. 이 클래스는 말그대로 자동으로 프록시를 생성해주는 빈 후 처리기이다.

 

빈에 Advisor 들을 등록해놓으면, 이 빈 후 처리기가 자동으로 어드바이저들을 찾아서 적용해준다. 이때, 어드바이저에는 어드바이스포인트컷이 모두 포함되어 있다. 여기서 어드바이스는 부가기능을 적용하는, 즉 꾸미기 아이템이고, 포인트컷은 어떤 스프링 빈에 프록시를 적용할지 결정해주는, 이름표같은 개념이다.

 

결론.

@Configuration, 설정 파일에 Advisor만 등록해주면, 알아서 스프링이 적용해준다.

먼저, Java 에서 JDK 동적 프록시란?

https://jinn-o.tistory.com/293

 

[JDK 동적 프록시] 프록시 객체들을 공통화 하기

먼저 프록시(Proxy)란?프록시란, 주로 실제 객체 앞에서 실행되는 대리 객체처럼 이해하면 된다. 다음 코드를 보면, RealService 객체에 대한 지연 초기화(Lazy Initialization)를 구현하고 있는 Proxy를 확

jinn-o.tistory.com

 

그렇다. 메인 로직 전후에 부가적인 작업들을 런타임 시점에 자동으로 생성하는 것이다.

 

JDK 동적 프록시는 InvocationHandler 를 이용해서 자동으로 프록시를 생성하게 할 수 있다. 그러나 이 기능은 인터페이스를 거쳐야만 사용이 가능하다. 그렇다고 클래스 만드로 자동으로 프록시를 생성할 수 없는 것은 아니다. CGLIB가 제공하는 MethodInterceptor를 사용하면 클래스만으로도 자동으로 프록시를 생성할 수 있다.

 

어떤 말인지 몰라도 상관없다.

 

핵심은 Spring 에서는 Advice 만 만들면 된다는 것이다.

JDK 동적 프록시가 제공하는 InvocationHandler든, CGLIB가 제공하는 MethodInterceptor든 상관없다. 스프링은 이 모든 자동 프록시 생성을 하나로 추상화 해놓았다. 우리는 Advice 만 이해하면 된다. 구체적으로 설명하자면, Spring은 JdkDynamicAopProxy와 CglibAopProxy를 사용해서 둘 모두 Advice를 최종적으로 호출하도록 어댑터 로직을 구현해놓았다.

 

org.aopalliance.intercept 패키지안에 있는 MethodInterceptor 인터페이스

이 인터페이스는 Advice를 상속하며, Advice를 생성하는 방법 중에 하나이다. MethodInvocation 파라미터를 통해서 JDK 동적 프록시에 있는 메소드처럼 메소드나 객체에 접근할 수 있는데, 이렇게 접근해서 파라미터를 연결해주는 모든 작업을 추상화 해놓았다. 따라서 MethodInvocation 객체의 proceed() 메소드만 호출하면 모든 일을 자동으로 연결시켜준다.

 

Advice를 직접 만들어보자.

package advice;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class TimeAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("=====TimeProxy 실행=====");
        long startTime = System.currentTimeMillis();

        // 실제 메서드를 호출한 후, 그 메서드의 반환값을 그대로 반환
        // 즉, 실제 메서드의 호출 및 결과 받기
        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("=====TimeProxy 종료 resultTime={}=====", resultTime);
        return result;
    }
}

 

이 Advice를 어떻게 실제 메서드와 연결하는가?

package advice;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.ProxyFactory;

@Slf4j
public class ProxyFactoryTest {

    public static void main(String[] args) {
        interfaceProxy();
    }

    static void interfaceProxy() {
        ServiceInterface target = new RealService();

        // 프록시를 매칭할 target (실제 서비스)를 넣어준다.
        ProxyFactory proxyFactory = new ProxyFactory(target);

        // 어드바이스 매칭
        proxyFactory.addAdvice(new TimeAdvice());

        // 어드바이스가 매칭된 프록시를 반환받는다.
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());

        proxy.call();
    }
}

interface ServiceInterface {
    String call();
}

class RealService implements ServiceInterface {
    @Override
    public String call() {
        return "RealService";
    }
}

 

실제 서비스가 아닌 프록시가 대신 실행된 것을 볼 수 있다.

스프링 AOP를 이해하려면 Proxy 에 대해 이해해야 한다.

그리고 스프링은, 프록시를 일일히 직접 생성하지 않는다.

 

그래서 스프링 AOP 에는 일명 프록시 생성 공장이 있다.

 

ProxyFactory, 프록시 생성 공장이 필요한 이유

개발자가 직접 일일이 프록시를 생성하려면 프록시마다 설정해야 할 코드가 반복적으로 필요하고, 특정 메서드에만 부가 기능을 추가할 때도 매번 별도의 작업이 필요하게 된다. Spring AOP가 이러한 불편함을 해소하기 위해 ProxyFactory 라는 프록시 생성 공장을 만들었다.

ProxyFactory는 이름 그대로 프록시를 생성하는 공장 역할을 한다. 즉, 개발자는 ProxyFactory에게 필요한 프록시 설정만 알려주면, 나머지는 ProxyFactory가 알아서 처리해 주는 구조이다. Spring은 ProxyFactory를 통해 프록시 생성과 관리 과정을 단순화할 수 있다.

 

 

ProxyFactory 에게 주어지는 작업 목록 : Advice

이제 프록시가 생성되면, 프록시가 해야할 일을 배정해주어야 한다. 이 일들을 정의해두는 일종의 규칙서가 Advice이다. 따라서 프록시는 메서드를 호출할 때마다 Advice에 정의된 작업을 수행한다.

 

MethodInterceptor ?

MethodInterceptor는 Advice의 일종으로, 메서드 호출 전후에 해야할 작업을 감싸는 역할을 한다. ProxyFactory에 MethodInterceptor를 설정해 두면, 프록시는 메서드 호출시 MethodInterceptor에 정의된 로직을 먼저 실행하게 된다. 예를 들어, 메서드를 호출할 때마다 로그를 남기거나, 트랜잭션을 시작하고 끝내는 작업을 추가할 수 있다. ProxyFactory는 이러한 MethodInterceptor를 포함한 프록시를 자동으로 생성하여 개발자가 직접 메서드 전후의 부가 작업을 관리할 필요가 없도록 해준다.

 

Proxy와 Advice를 조합해주는 프록시 생성 공장, ProxyFactory.

따라서 ProxyFactory는 단순히 프록시를 생성하는 역할을 넘어, 프록시와 Advice를 조합해 주는 유연한 시스템을 제공한다. 개발자는 특정 클래스에 여러 개의 Advice를 적용하거나, 필요한 시점에 따라 Before Advice, After Advice, Around Advice 등 다양한 Advice를 프록시에 추가할 수 있다.

 

이처럼 ProxyFactory는 프록시 생성과 동시에, 필요한 Advice를 설정하고 관리하는 편리한 시스템으로, Spring AOP에서 핵심적인 역할을 수행한다.


QueryDSL 값이 존재하는지 boolean 값만 반환하고 싶을때.

보통 쿼리에서 true/false 값만 반환한다고 하면..
해당 값이 존재하는지 아닌지의 여부만 판단하고 싶을 때 일 것이다.

여느 때와 같이 queryDSL 을 짜고 있었는데 ...

값이 존재하는지 아닌지 여부만 반환하고 싶은데,

CASE 문을 통해서 해야 하나?

public boolean hasRegisteredUser(Long custGroupSeq) {
    return jpaQueryFactory.select(
                new CaseBuilder()
                	.when(custGroup.count().gt(0).then(true)
                    .otherwise(false)
            )
            .from(custGroup)
            .where(custGroup.custGroupSeq.eq(custGroupSeq))
            .fetchOne();
}

 

처음엔 이렇게 쿼리를 짰었는데..

"Unboxing of * may produce 'NullPointerException

 

이런 찝찝한 경고문이 떠서 이 방법은 안전하지 않은 것 같았다.

 

답은 생각보다 간단하게 나왔다.

마지막에 fetch().size() > 0 을 붙여보고 깨달았다.

그냥 이렇게 하자.

 

참고문서 (나랑 비슷한 고민을 한 개발자가 있었다.)

https://stackoverflow.com/questions/64910252/querydsl-how-to-use-exist-to-return-boolean

 

QueryDsl how to use exist() to return boolean

I want to upgrade queryDsl from verision 3 to version 4. But i've an issue with exist() In QueryDsl 3 exist return a Boolean but in QUeryDSL 4 it return a BooleanOperation. In my case i really wan...

stackoverflow.com

 

 

결론

public boolean hasRegisteredUser(Long custGroupSeq) {
    return !jpaQueryFactory
            .selectFrom(custGroup)
            .where(custGroup.custGroupSeq.eq(custGroupSeq))
            .fetch().isEmpty();
}

 

 

쿼리가 훨~씬 깔끔해지고 안정성도 올라갔다!

그 어느 날과 같이 평범하게 개발을 하고 있던 날..

QueryDSL 에서 Projections 를 이용하여 DTO로 변환하는 과정을 개발하고 있었다.

그렇다.
오류가 발생했다.

 

class com.querydsl.core.types.QBean cannot access a member of class com.xxx(패키지명) with modifiers "protected"

 

 

대충 보아하니 접근제한자를 private 로 지정한 무언가에 대해 접근할 수 없어서 protected 로 변경하라는 거 같은데,

예상과 맞게도 문제의 원인은 다음과 같았다.

 

 

QueryDSL 프레임워크인 QBean이 클래스 com.xxx에서 접근수준이 protected 이상으로 선언된 멤버(필드 또는 메서드)에 접근하려고 하는데, 접근수준(접근제한자) 가 private로 되어 있어서 접근을 하지 못한 것 이었다.

 

 

그런데 왜 QueryDSL의 QBean이 접근수준이 private인 멤버에 접근하려고 한 것일까?

 

QueryDSL 구문을 짤때, 엔티티 자체로 반환하는 것을 막기 위해서 DTO로 변환해서 반환하곤 한다.

나도 그렇게 개발하고 있었고...

 

 

결론을 설명하기 전에, 엔티티를 DTO로 변환해주는 Projections 클래스에 대해서 알아보자.

먼저 QueryDSL에서 엔티티를 DTO로 변환하여 반환하는 방법은 Projections 클래스를 이용해서, 크게 3가지가 있다.

 

① Projections.bean(someDto.class, ...변환하고 싶은 setter가 존재하는 필드 나열)

즉, Setter 메서드를 통해서 접근하는 방법

import static com.querydsl.core.types.Projections.bean;

QBook qBook = QBook.book;

List<BookDTO> bookDTOs = queryFactory
    .select(bean(BookDTO.class, qBook.title, qBook.author.name.as("authorName")))
    .from(qBook)
    .fetch();

 

② Projections.fields(someDto.class, ...변환하고 싶은 필드 나열)

즉, DTO의 필드명을 직접 지정하여 필드에 접근하는 방법

import static com.querydsl.core.types.Projections.fields;

QBook qBook = QBook.book;

List<BookDTO> bookDTOs = queryFactory
    .select(fields(BookDTO.class, qBook.title, qBook.author.name.as("authorName")))
    .from(qBook)
    .fetch();

 

 

③ Projections.constructor(someDto.class, ...선언한 생성자의 필드 나열)

즉, DTO 클래스의 생성자를 호출하여 DTO 객체를 생성하는 방법

import static com.querydsl.core.types.Projections.constructor;

QBook qBook = QBook.book;

List<BookDTO> bookDTOs = queryFactory
    .select(constructor(BookDTO.class, qBook.title, qBook.author.name.as("authorName")))
    .from(qBook)
    .fetch();

 

 

 

기본적으로 Projections는 인자 값이 없는 기본 생성자와 프로퍼티의 Getter/Setter의 접근을 반드시 필요로 하는데, 기본 생성자를 Protected 로 지정해버리면 해당 메서드에 접근할 수 없어 오류가 나는 것이다. (보통 getter/setter는 public 으로 지정하니, 문제가 없을 것이라 생각한다.)

 

 

결론

 

 

DTO 의 @NoArgsConstructor 는 접근레벨=PUBLIC 으로 선언하자.


엔티티를 선언할때, 기본생성자를 지정할때 PROTECTED로 선언하는 것이 보안상 안전하기 때문에 PROTECTED 로 선언하던 습관을 DTO에도 적용하니까 문제가 발생했던 것이다.

사건의 발단

React 단에서 boolean 컬럼 값이 포함된 JSON 객체를 담아 POST 요청을 보냈고,
Spring 에서는 해당 객체를 boolean 컬럼이 포함된 DTO 객체로 @RequestBody 응답을 받았다.

그런데...., 이상하게도!

다른 모든 컬럼 값은 제대로 받아지는데, boolean 값만 자꾸 기본값인 false로 받아지는 것이었다.

무언가.. boolean 값만 파라미터 바인딩에 문제가 생긴 것이 분명했다.

원인은 뭐였을까?

 

DTO 내부에 boolean 컬럼 값의 경우 `is` 접두사로 시작했기 때문이었다.

 

 

이게 왜 안될까?

 

좀더 본질적인 원인은 Lombok 의 @Getter 동작방식에 있었다.

다음은 Lombok 공식문서에서 @Getter @Setter 에 관한 내용이다.

 

 

https://projectlombok.org/features/GetterSetter

 

@Getter and @Setter

 

projectlombok.org

 

 

For boolean fields that start with is immediately followed by a titlecase letter, nothing is prefixed to generate the getter name.

Any variation on boolean will not result in using the is prefix instead of the get prefix; for example, returning java.lang.Boolean results in a get prefix, not an is prefix.

 

 

해석해보면 다음과 같다.

 

boolean 타입의 필드가 is로 시작하고 그 다음에 대문자가 오는 경우, getter 메서드 이름에 아무 것도 접두사로 붙이지 않습니다. 즉, 필드 이름 그대로 사용합니다.

boolean 타입이 아닌 java.lang.Boolean과 같은 타입을 반환하는 경우, getter 메서드는 is가 아닌 get 접두사를 사용합니다.

 

 

 

정리하자면,

 

Lombok 에서는 @Getter @Setter 어노테이션을 이용해서 getter/setter 메소드를 자동으로 생성해주는데, 이때 boolean 타입은 getter 바인딩 시에 `get` 이 아닌 `is` 접두사를 붙여서 getter 메서드를 생성해주기 때문에 내부적으로 is 가 두 번 호출되는 이상한 네이밍의 메서드가 생성되었기 때문에 DTO가 인식할 수 없었던 것이다.

 

 

JSON 필드명과 DTO 필드명이 일치하지 않으면 Jackson이 JSON 필드를 DTO 필드에 매핑하지 못한다. 예를 들어, JSON 필드가 "isSomeBoolean": true이고 DTO 필드가 private boolean isSomeBoolean;이라면, Jackson은 이를 자동으로 매핑하지 못한다.

 

 

Java Bean 규약에 따르면 boolean 타입의 변수에 대한 getter 메서드는 is로 시작하고, setter 메서드는 set으로 시작해야 한다. 하지만 필드 이름 자체에 is를 포함시켜버리면 규약에 맞지 않게 되면서, 바인딩이 잘 되지 않는 상황이 발생하는 것이다.

Spring Security 란?

Spring 에서 제공하는 강력한 보안 기능이다. 특히, 커스텀이 가능한 인증(Authentication)과 인가(Authorization)의 두 가지 주요 기능을 제공한다.

 


인증 (Authentication)

사용자가 "누구"인지 확인하는 과정이다. 자신을 증명하기 위해서 자격 증명(ex. 사용자 이름이나 비밀번호)가 필요하다.

 

 

인가 (Authorization)

인증된 사용자가 특정 자원에 접근하거나 특정 작업을 수행할 수 있는 "권한"을 가지고 있는지 결정하는 것이다.

  • 경로 기반 인가 (특정 HTTP 경로)
  • 메소드 시큐리티 (Java 메소드 호출)
  • 표현식 기반 접근 제어 (보다 복잡한 규칙을 표현식으로 정의)

 

 

 

 

 

 

 

로그인 방식에는 크게 두가지로 나눌 수 있다. 바로 상태를 유지하는 인증(Stateful Authentication)상태를 유지하지 않는 인증(Stateless Authentication)이다.

 

상태를 유지하는 인증 (Stateful Authentication)

서버가 사용자의 로그인 상태를 확인할 때 쿠키나 세션을 사용하는 방식을 말한다. 사용자의 인증 상태는 서버에서 관리된다.

  • 폼 기반 로그인 방식 (Form-based Authentication, 가장 일반적)
    가장 일반적으로 사용되며, 사용자가 웹 폼(Form)을 통해 로그인 정보(아이디, 비밀번호)를 입력하는 방식이다. 서버는 이 정보를 받아 인증을 수행하고, 성공적인 인증 후에는 사용자의 세션을 생성하여 로그인 상태를 유지한다.

  • LDAP (Lightweight Directory Access Protocol, 내부 네트워크 또는 기업 환경에서 주로 쓰임)
    폼 기반 또는 다른 인증 방식을 통해 LDAP 서버에 접근하여 사용자의 자격 증명을 확인하는 방식이다.



상태를 유지하지 않는 인증 (Stateless Authentication)

상태를 유지하지 않는 인증은 서버가 사용자의 인증 상태를 세션에 저장하지 않는다. 대신, 클라이언트가 서버로 요청을 보낼 때마다 인증 정보를 포함하여 자격을 증명한다.

 

  • HTTP 기반 로그인 방식(HTTP Basic Authentication)
    HTTP 헤더사용자의 이름과 비밀번호를 Base64로 인코딩하여 전송한다. 서버는 매 요청마다 이 정보를 확인하여 인증을 수행한다. 인증정보는 저장되지 않으며, 각 요청은 독립적으로 처리된다.
  • OAuth2 (구글, Facebook API)
    외부 서비스 제공자를 통해 인증하는, 제 3자가 끼어드는 인증 방식이다. 사용자가 서비스 제공자를 통해 인증하고, 인증에 성공하면 토큰을 받는다. 이 토큰은 사용자의 요청마다 서버에 제출되어 인증을 위해 사용된다.

  • JWT (JSON Web Tokens)
    사용자가 인증하면, 서버는 JSON 형태의 웹 토큰을 생성하고 반환한다. 이 토큰은 사용자가 서버에 요청을 보낼 때마다 헤더에 포함시켜 전송한다. 토큰 자체에 사용자의 인증 정보가 포함되어 있어, 서버는 세션을 유지할 필요가 없다.

 

 

오늘은 스프링에서 자주 사용하는 Model 객체와 ModelMap 객체에 대해서 포스팅 해보겠다.

Model만 있는 줄 알았는데 ModelMap 이라는 객체도 있었다.


1. 스프링에서 Model 객체란?

스프링에서는 Controller 의 메소드들이 받는 파라미터(Argument)들을 관리하는 ArgumentResolver 가 존재한다. (정확하게는 HandlerMethodArgumentResolver 클래스에서 Argument들을 관리하는 역할을 한다.) 여기에 등록되어 있는, 자주 사용되는 파라미터 중 하나라고 할 수 있다. 주로 데이터를 관리하기 위한 파라미터이다. (MVC 패턴에서 M도 Model의 의미를 지니는 데, 그와 비슷하게 Model 객체도 Data를 전달하고 관리하기 위한 Argument 이다.)

 

Model과 ModelMap 두 객체는 기본적으로 상당히 비슷한 기능을 제공한다.

addAttribute(...) 메소드를 사용해서 다루고 싶은 데이터를 넣고,

getAttribute(...) 메소드를 이용해서 넣었던 데이터들을 빼서 사용할 수 있다.

 

 

2. Model  객체는 interface 이다.

public interface Model {
    
    Model addAttribute(String attributeName, @Nullable Object attributeValue);

	...
    
    @Nullable
    Object getAttribute(String attributeName);
}

Model 객체는 인터페이스로 구현되어 있기 때문에 map 인터페이스 구현체로의 변경이 쉬워서 코드의 유연성이 높다는 특징이 있다. 예를들어 TreeMap 이나 HashMap 으로도 호출할 수 있다.
(참고로, 페이지 redirect 시에 RUL parameter 정보들을 쉽게 관리하게 해주는 RedirectAttributes 인터페이스도 Model 인터페이스를 상속받는다.)

3. ModelMap 객체는 Class 이다. (LinkedHashMap)

public class ModelMap extends LinkedHashMap<String, Object> {

    public ModelMap addAttribute(String attributeName, @Nullable Object attributeValue) {
        Assert.notNull(attributeName, "Model attribute name must not be null");
        this.put(attributeName, attributeValue);
        return this;
    }

	...

    @Nullable
    public Object getAttribute(String attributeName) {
        return this.get(attributeName);
    }
}


구체적인 클래스로 구현되어 있기 때문에, LinkedHashMap 을 상속받은 클래스로 사용이 가능하다.

 

LinkedHashMap 클래스의 경우 HashMap 클래스를 상속받고 있는데, 이 둘(LinkedHashMap 과 HashMap)의 차이점은 순서의 유무이다. HashMap의 경우 순서 고려 없이 Entry에 Node 데이터를 집어 넣지만, LinkedHashMap의 경우 before, After Entry를 지정하여 put 한 순서데로 데이터를 저장한다는 차이점이 있다.

 

4. HandlerMethodArgumentResolver에 Model객체는 어떻게 등록되어 있을까?

public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
    public ModelMethodProcessor() {
    }

	// parameter 타입 특정하기 (간소화함)
    public boolean supportsParameter(MethodParameter parameter) {
        return Model.class;
    }

	// parameter 로써 역할 작성하기
    @Nullable
    public Object resolveArgument(...) throws Exception {
        ...
    }

	// return 타입 특정하기 (간소화함)
    public boolean supportsReturnType(MethodParameter returnType) {
        return Model.class;
    }

	// return 값으로써 역할 작성하기
    public void handleReturnValue(...) throws Exception {
        ...
    }
}

 

Model.class 의 경우 ModelMethodProcessor 클래스로 구현되어 있다. (참고로 함께 상속되어 있는 HandlerMethodReturnValueHandler 는 파라미터가 아니라 반환할 때의 데이터를 관리하기 위한 핸들러이다.)

 

기본적으로 스프링은, 컨트롤러의 파라미터들을 가져올 때 HandlerMethodArgumentResolver 를 상속받은 클래스들을 탐색한다. 마찬가지로 컨트롤러의 반환값들을 가져올 때는 HandlerMethodReturnValueHandler 인터페이스를 상속받은 클래스들을 탐색한다.

 

여기에 등록이 되어있어야 스프링이 파라미터나 반환값으로 인식할 수 있는 것이다.

 

ec2 linux 서버에 pm2 3000번 port 의 react 프로젝트를 배포하려고 했다.

이때 pm2를 이용해서 무중단 배포를 해보려고 했다.

 

그런데 .... 자꾸 이런 에러가 났다.

 

PM2 | Error caught while calling pidusage
PM2 | TypeError: One of the pids provided is invalid
PM2 | at get (/usr/local/lib/node_modules/pm2/node_modules/pidusage/lib/stats.js:78:23)

 

1차 시도 )) node js 삭제후 다시 깔기.

node 를 -g로 깔아야한다. 부터 시작해서 version 문제라고 구글링 했으니 나왔기 때문이었다.

sudo npm uninstall pm2 -g

먼저.. pm2 부터 삭제하고..

sudo yum remove npm
sudo yum remove nodejs

 

node를 차례로 삭제했다.

 

node -v
npm -v

삭제 결과도 확인하고.. -v를 이용해서 version을 확인했는데 없는 command라고 나오는걸 보니 삭제가 잘 되었다.

 

sudo yum install nodejs@latest

재설치...

 

 

근데 여기서 뭔가 쎄했다.

그냥 처음에 설치한 그대로 다시 하고 있는 기분.

버전 업그레이드는 안된 것 같고..

왜 Linux 에는 가장 최신 버전이 .. 10.2.0 인거지??

 

아니나 다를까.

다시 설치해도 오류는 그대로였다.

 

 

2차 시도 )) Linux에서 node 20으로 버전 업그레이드를 어떻게 하지 !??! 해답은 NVM

역시 예상대로 ubuntu 앱스토어 상의 node 최신 버전은 v10.19.0 이라고 한다.

하지만 실제로 최신 노드 버전은 v20까지 나왔다.

이 현상을 해결하기 위해서는 nvm가 필요했다.

 

nvm은 Node Version Manager 이다.

우리는 이제 nvm을 이용해서 버전을 업그레이드 할 것 이다.

# installs NVM (Node Version Manager)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# download and install Node.js
nvm install 20

# verifies the right Node.js version is in the environment
node -v # should print `v20.11.1`

# verifies the right NPM version is in the environment
npm -v # should print `10.2.4`

nvm 공식문서 내용을 그대로 가져왔다.

 

나는 이런 방식을 이용해서 node v20.11.1 을 성공적으로 설치할 수 있었다.

 

 

3차 시도 )) 이번엔 pm2가 문제?! 업그레이드.... 

해결하고나니까 또 다른 문제가 터졌다.

cannot find module pm2/lib/ProcessContainerFork.js

 

음.. 하지만 이 문제는 생각보다 단순했다.

pm2 upgrade

pm2 버전문제였다.

 

 

오늘의 결론..

다 해결하고 나니 다 버전문제였네

 

 

참고 문서

https://github.com/Unitech/pm2/issues/5496

 

TypeError: One of the pids provided is invalid · Issue #5496 · Unitech/pm2

What's going wrong? Trying to run a ts script, it doesn't work, but I am sure outside of pm2 works, checking the log the error is: PM2 | Error caught while calling pidusage PM2 | TypeError: One of ...

github.com

https://www.freecodecamp.org/korean/news/how-to-install-node-js-on-ubuntu-and-update-npm-to-the-latest-version/

 

Ubuntu에 Node.js를 설치하고 npm을 최신 버전으로 업데이트하는 방법

apt-package manager를 사용하여 최신 버전의 노드를 설치하려고 하면 v10.19.0이 설치됩니다. 이것은 ubuntu 앱스토어의 최신 버전이지만, NodeJS의 최신 버전은 아닙니다. 새로운 버전의 소프트웨어가 출

www.freecodecamp.org

 

+ Recent posts