반응형

 

Spring Security를 공부하다가 문득 나온 Filter

얼핏 보았을때 뭔가를 거른다 ? 라는 생각이 들었다.

사실을 뭔가 ? 비슷하다 

 

사실 Filter는 스프링만의 기능이 아닌 자바 서블릿에서도 제공하는 기능이지만

스프링 시큐리티에서 다양하게 활용된다.

Filter가 어디에 사용될까 ?

스프링 시큐리티에서 필터는

클라이언트가 서버에게 요청을 보내면  DispatcherServlet(FrontController) 로 요청이 전달되어 여러가지의 관문들을 통해 

다시 클라이언트는 응답을 받게 되는데

Filter는 요청이 DispatcherServlet에 닿기전 , DispatcherServlet에서 응답이 나온후에 인증 ,권한 ,보안 처리를 하게된다.

쉽게 말해서 이상한 요청 거르고 필요한 정보 추가해서 보내고 받는 기능을 제공한다

 

Filter 의 종류

Spring을 하면서 아직도 헷갈리는 개념인데

같은 이름이여도 여러가지의 interface로 너무나도 많은 기능들이 존재한다.

SecurityContextPersistenceFilter -> 요청 전에 SecurityContextRepository에서 받아온 정보를 SecurityContextHolder에 넣는다.

LogoutFilter -> 로그아웃을 진행했는지 확인한다.

UsernamePasswordAuthenticationFilter -> login을 시도하게 되면 이 Filter를 거쳐간다

등 이 3가지 이외에도 여러가지의 필터들이 있다

궁금하면 찾아보자 

하지만 이러한것들을 하나하나 다 구현할 필요는 없고

내가 사용해야는 기능을 단지 implement해서 사용하면 된다.

 

Filter의 사용방법

아래와 같이 만들어져있는 기능이 아닌 새롭게 내가 만들어서 사용한다면 

가장 basic한 Filter을 구현하면 된다.

Filter의 doFilter를 오버라이딩해서 사용하는데

여기서 chain.doFilter(request , response) 를 꼭 입력해주어야 다음 필터로 넘어가는

필터 체인이 형성된다.

 

public class MyFilter1 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("필터1 ");

        chain.doFilter(request , response);
    }
}
public class MyFilter2 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("필터2 ");

        chain.doFilter(request , response);
    }
}

 

 

처음에는 설정 한다는게 참 이해가 안가고 어려웠지만 

조금은 ? 익숙해진 @Configuration 

FitlerConfig class를 생성해주고 필터에 대한 설정을 진행해준다

위에 선언되어있는 필터들을 @Bean으로 등록해주고 각각 FilterRegistrationBean<FilterName> 을 선언해준다.

addUrlPatterns : 반응할  Url을 선언해준다.

setOrder(number) -> 낮은 번호가 먼저 실행된다.

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<MyFilter1> filter1(){
        FilterRegistrationBean<MyFilter1> bean = new FilterRegistrationBean<>(new MyFilter1());
        bean.addUrlPatterns("/*");
        bean.setOrder(1); // 낮은 번호가 필터중에서 가장 먼저 실행됨
        return bean;
    }

    @Bean
    public FilterRegistrationBean<MyFilter2> filter2(){
        FilterRegistrationBean<MyFilter2> bean = new FilterRegistrationBean<>(new MyFilter2());
        bean.addUrlPatterns("/*");
        bean.setOrder(0); // 낮은 번호가 필터중에서 가장 먼저 실행됨
        return bean;
    }
}

 

 

당연하게도 SecurityConfig에서도 필터를 사용할수 있다.

하지만 필터체인보다 시큐리티 필터 체인이 먼저 사용된다는 점이 있다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        http
//                .addFilterBefore(new MyFilter2() , BasicAuthenticationFilter.class); 기본적으로 필터체인보다 시큐리티필터체인이 먼저 사용된다.
                .csrf().disable().cors().disable()
                .httpBasic(basic -> basic
                        .disable())
                .sessionManagement(sessoin -> sessoin
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilter(new JwtAuthenticationFilter(authenticationManager))
                .authorizeHttpRequests(request -> request
                        .requestMatchers("/api/v1/user/**").authenticated()
                        .requestMatchers("/api/v1/manager/**").hasAnyRole("MANAGER" , "ADMIN")
                        .requestMatchers("/api/v1/admin/**").hasAnyRole( "ADMIN")
                        .anyRequest().permitAll()
                )
                .formLogin().disable();

        return http.build();

    }

 

 

UsernamePasswordAuthenticationFilter 동작순서

 

위에서도 말한 UsernamePasswordAuthenticationFilter -> login을 시도하게 되면 이 Filter를 거쳐간다

동작순서는 다음과 같다

1.login을 username, password(Post)전송해서 요청을한다면

2.UsernamePasswordAuthenticationFilter가 실행되고 UsernamePasswordAuthenticationToken 객체가 생성되고

3.UsernamePasswordAuthenticationToken을 AuthenticationManager에게 전달해 실제로 사용자를 인증하도록한다.

4.이후 UserDetailsService를 구현한 클래스에서 loadUserByUsername() 메소드를 통해서 사용자 정보를 가져오고 비밀번호를 비교하여 인증을 시도한다.

5.만약 성공하면 SecurityContextHolder 에 인증된 사용자 정보 저장

성공시 -> AuthenticationSuccessHandler

실패시 -> AuthenticationFailureHandler 동작을 수행

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("로그인 시도 : jwtFilter");
        return super.attemptAuthentication(request, response);

    }
}

 

 

결론

필터를 등록하면 요청, 응답 하기전에 기능들을 넣을수 있다.

반응형
반응형

UserDetails

UserDetails interface는 스프링 시큐리티가 사용자의 인증 및 권한 부여를 위해 필요한 정보를제공하기 위한 인터페이스

결국 사용자 정보를 효과적으로 이해하고 이를 기반으로 보안 구현

기본적인 메소드를 스프링 시큐리티에서 제공 거기에다가 조금 Custom 해서 내 마음대로 바꿔서 사용

뼈대에 살붙이기 -> 편리하다 

 

저장 위치

SecurityContextHolder -> Context -> Authentication -> Principal  -> UserDetails 객체가 저장되어있다

 

아래와 같이 UserDetails를 구현하는 PrincipalDetails class 를 생성해서 사용한다.

주의!! 모든 오버라이드 메소드는 자신이 변경해서 사용이 가능하다 

 

getAuthorities() 부분에서 ROLE_ADMIN ,ROLE_USER 형식으로 반환해 주어야한다

// Security Session => Authentication => UserDetails(PrincipalDetails)
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails {

    private final User user;

    //해당 User의 권한을 리턴하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collect;
    }
    //비밀번호
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    //User의 이메일도 되지만 PK값을 넘기면 중복을 없앨수 있다
    @Override
    public Long getUsername() {
//        return user.getUsername();
        return user.getId();
    }

    //계정 만료 여부
    //true : 만료 안됨
    //false : 만료
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //계정 잠김 여부
    //true : 잠기지않음
    //false : 잠김
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //비밀번호 만료 여부
    //true : 만료 안됨
    //false : 만료
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //1년동안 회원이 로그인을 안하면 휴먼 계정으로 변경
    //true : 활성화
    //false : 비활성화
    @Override
    public boolean isEnabled() {
        //User Entity에 Timestamp loginDate 만들고
        //현재시간 - 로그인시간 => 1년 초과하면 return false; 하는 과정으로
        //휴면 계정을 판단할 수 있다.
        return true;
    }
}

 

 

 

UserDetailsService 

동작 방법

사용자가 로그인을 할때 스프링 시큐리티에서는 loadUserByUsername 메소드를 호출하게 된다.

따라서 우리는 loadUserByUsername 메서드를 가지고 있는 UserDetailsService 인터페이스를 구현하는 새로운 클래스를 만들어서 

자신만의 loadUserByUsername의 기능을 구현해주어야 한다.

 

Spring Security의 인증 프로세스는 UserDtailsService를 사용해서 사용자를 검새후 UserDetails객체를 생성한다

위에서 보다시피 UserDetails 객체 : 사용자의 인증 및 권한 부여에 필요한 정보를 담고있다.

유저 정보 -> UserDetailsService(loadUserByUsername) -> UserDetails 객체 생성

 

UserDetailsService를 구현하면 loadUserByUsername 메서드를 제공할 수 있는데 이 메서드는 사용자 이름을 받아

해당 사용자의 정보를 검색한다.

 

비밀번호 검증은 시큐리티가 UsernameAndPasswordToken로 내부에서 알아서 검증해준다

구현한 모습

//시큐리티 session = Authentication = UserDetails
//principalDetails -> UserDetails type
//시큐리티 설정에서 loginProcessingUrl("login");
//login 요청이 오면 자동으로 UserDetailsService타입으로 IoC 되어 있는 loadUserByUsername함수가 실행

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //알다시피 findBy규칙 Username 문법
        // select * from user where username = ? 쿼리가 실행된다. -> jpa 에서 알아서 메소드 이름 기준으로 생성해줌
        User userEntity = userRepository.findByUsername(username);
        // 이 부분에서 유저 정보에 대한 값 세팅 로직을 작성
        if (userEntity != null){
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
}

 

반응형
반응형

@Configuration

SecurityConfig은 설정 파일이니

@Configuration을 사용해서 스프링에게 설정 파일이라는 사실을 알려준다

-> @Configuration을 가진 클래스는 스프링 컨테이너에 의해 관리되며  @Bean정의와 구성요소를 제공하게 된다.

 

 

예전 버전에서는 

WebSecurityConfigurerAdapter을 상속받아서 사용했지만 이제는 상속받아서 사용하는 것이 아닌 -> @EnableWebSecurity로 대체
아래와 같이 @Bean으로 등록한 후 filterChain method 을 설정해서 사용하게 된다.

 

 

filterChain(HttpSecurity http) 메소드에서 HttpSecurity을 통해서 http에 대한 설정을하고 

SecurityFilterChain을 반환하게 된다면 

특정 URL패턴이나 규칙에 따라 다른 필터들을 적용할 수 있도록 된다.

HttpSecurity

spring Security에서 사용되는 클래스로,

웹 기반 보안에 대해 설정을 구성하는데 사용된다.

-> filterChain  :  인증 , 인가 , 세션 ,로그인 기능을 정의하는 메소드

 

Httpsecurity를 통해서 설정하는 여러가지 방법들이 있는데 

1. 인증설정

FormLogin : Form 기반 인증을 구성한다.

  • loginPage(/loginForm) : 사용자정의 로그인 페이지를 지정한다.
  • loginProcessingurl("/login") : 로그인 양식이 제출되는 URL을 설정
  • defaultSuccessUrl("/") : 로그인이 성공적으로 진행 되었다면 리디렉션할 URL
http
    .formLogin() // 폼 기반 로그인 설정
    .loginPage("/loginForm") // 로그인 페이지 경로
    .loginProcessingUrl("/login") // 로그인 처리 URL
    .defaultSuccessUrl("/") // 로그인 성공 후 이동할 URL
    .and()
    .httpBasic().disable(); // HTTP 기본 인증 비활성화

 

2.인가 설정

 

 

authorizeRequests() : 권한 부여 구성을 시작한다.

  • requestMatchers : URL 경로를 입력해주고
  • permitAll() , authenticated() , hasRole() 여러가지의 접근 제한 설정 가능 
  • 그 뒤에 해당하는 보안요소를 추가해준다 권한이 있다거나 , 모두 입장이 된다거나 , 아무도 못들어간다거나 ,,,,
  • ex ) requestMatchers.hasAnyRole("ADMIN")
http
    .authorizeRequests()
        .requestMatchers("/public/**").permitAll() // 특정 URL 패턴에 대한 전체 접근 허용
        .requestMatchers("/user/**").authenticated() // 인증된 사용자만이 접근 허용
        .requestMatchers("/admin/**").hasRole("ADMIN") // "ADMIN" 역할을 가진 사용자만이 접근 허용
        .anyRequest().permitAll(); // 나머지 URL에 대한 접근은 허용

 

 

3.  CSRF 설정 

CSRF : Cross-Site Request Forgery 공격

HttpBasic.disable() : HTTP기본 인증을 비활성화 한다.

http
    .csrf().disable(); // CSRF 공격 방어 비활성화

 

4. 세션관리 설정

세션 생성 정책, 세션 고정 보호 ,최대 세션 허용등을 구성

http
    .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션 생성 정책 설정 (STATELESS: 세션 사용 안 함)

 

5.이외의 설정

permitAll()

커스텀 필터 적용 : 

addFilterBefore() , addFilterAfter() 메소드를 사용해서 필터 적용 가능

특정 요청에 대한 추가적인 처리가 가능해진다.

http
    .addFilterBefore(new MyCustomFilter(), BasicAuthenticationFilter.class);

 

 

spring 3.x 버전 SecurityConfig의 전체적인 모습

 

@Configuration
@EnableWebSecurity // -> 스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {



    //헤당 메서드의 리턴되는 오브젝트를 IOC로 등록해줌
    //@Bean 어노테이션을 사용하면 다른 파일에서도 사용이 가능해 진다.
    @Bean
    public BCryptPasswordEncoder encoderPwd(){
        return new BCryptPasswordEncoder();
    }
    //hasAnyRole에는 ROLE_ 이포함되면안되지만 User Entity에는 포함되어야한다.(ROLE_USER)
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .csrf().disable()
//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//                .and()
                .formLogin()
                .loginPage("/loginForm")
                .loginProcessingUrl("/login") // /login가 호출되면 시큐리티가 낚아채서 대신 로그인 진행
                .defaultSuccessUrl("/") //성공하면 여기로 이동
                .and()
                .httpBasic().disable()
//                .apply(new MyCustomDsl()) // 커스텀 필터 등록
//                .and()
                .authorizeRequests()
                .requestMatchers("/user/**").authenticated()
                .requestMatchers("/manager/**").hasAnyRole("MANAGER" , "ADMIN")
                .requestMatchers("/admin/**").hasAnyRole( "ADMIN")
                .anyRequest().permitAll();

                return http.build();

    }


}

 

 

결론 

 

Spring Security에서 SecurityConfig class가 구성하는 방법 

 

1. Security Configuration

애플리케이션이 시작 될때  Security Configuration 클래스가 로딩

@EnableWebSecurity 가 있다면 Spring Security가 활성화 된다.

 

2.SecurityFilterChain Bean 생성

SecurityConfig 에서 FilterChain Bean을 정의 -> IOC 컨테이너에 등록됨

 

3.FilterChain 구성 

FilterChain 내에서 HttpSecurity 객체를 통해 필터 체인을 구성한다.

 

 

추가적인 기능들 

BCryptPasswordEncoder

기본적으로 SecurityConfig class파일에서 @Bean으로 등록해서 사용한다.

BCryptPasswordEncoder : 비밀번호를 안전하게 해시화하는 기능을 제공하는 인코딩(암호화)class

BCrypt는 해시 함수의 일종 -> 결국 비밀번호 해시화해서 보안성 높임

BCrypt의 특징

  • 각 비밀번호에 무작의 Salt값을 부여하여 동일한 비밀번호에 대해서 항상 다른 해시값을 사용 -> 레인보우 테이블 공격에 대해 방어가능
  • 해쉬화 할때 여러번의 라운드를 통해 고도의 연산 비용을 요구 -> 브루트 포스 공격에 대한 보안성 높임
    @Bean
    public BCryptPasswordEncoder encoderPwd(){
        return new BCryptPasswordEncoder();
    }

 

@Bean을 등록햇으니 

다른 클래스에서 이러한 형태로 사용이 가능해진다.

encode(CharSequence rawPassword) : 주어진 비밀번호를 해시화하여 반환

mathes(CharSequence rawPassword , String encodedPassword) : 주어진 비밀번호가 해당 해시 값과 일치하는지 확인

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

 

@EnableWebSecurity

Spring Security의 웹 보안 설정을 활성화하는데 사용된다.

@EnableWebSecurity을 사용하면 WebSecurityConfigurerAdapter를 확장한 클래스를 생성할 수 있다.

이 어노테이션 안에 여러가지의 설정 class들이 포함되어있다

간단하게 이 어노테이션을 사용해서 웹 보안 설정을 시작할 수 있다.

@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
		HttpSecurityConfiguration.class })
@EnableGlobalAuthentication
public @interface EnableWebSecurity {

	/**
	 * Controls debugging support for Spring Security. Default is false.
	 * @return if true, enables debug support with Spring Security
	 */
	boolean debug() default false;

}

 

@EnableMethodSecurity(securedEnabled = ture)

이 어노테이션을 사용하면 메소드 수준의 보안을 활성화 할 수 있다.

지금까지는 전체적인 것에 대해서 설정이엿다면

이 어노테이션을 선언해주고 해당 Controller의 메소드에 @Secured(접근 권한)을 설정해주면 메소드 단위로 접근제한 설정이 가능하다

    @Secured("ROLE_ADMIN")
    @GetMapping("/news")
    public @ResponseBody String news(){
        return "새로운 소식";
    }

 

반응형
반응형

 

벌크성 쿼리의 필요성 

예를 들어 일반적으로는 한개의 Member데이터를 가져와 Member의 이름을 변경하는 로직을 통해 값을 바꾸면 

트랜잭션 커밋 시점에 업데이트 쿼리가 나가게된다.

하지만 이런 방식은 모든 Member의 나이를 +1할때는 엄청난 쿼리가 나가게 된다.

 

벌크성 쿼리란?

모든 Member의 나이를 +1할때 한번의 쿼리로 모든 데이터 적용하는 쿼리를 말한다.

즉 모든 데이터를 변경하는 쿼리라고 생각하면 편할거같다.

 

@Modifying
 @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

벌크성 쿼리는 이러한 형태로 볼 수 있다

파라미터 이상인 데이터에 대해 모두 나이를 증가시키는 로직이다.

 

@Modifying

@Query에서 데이터를 변경할때는 값이 변경되었다고 @Modifying 을 활용해서 알려주어야 한다

 

위의 코드의 문제점

위의 코드가 문제점이 없는것 처럼 보이지만

엄청난 문제가 존재한다.

 

벌크 쿼리를 통해서 DB에 직접적으로(영속성컨텍스트 안거침) 쿼리를 날려 DB에 있는 데이터를 변경햇다고 치자

하지만 JPA에서 Member를 조회하면 변경된 값이 나오지않게 된다.

변경된 값이 나오지 않는 이유는 영속성 컨텍스트 때문인데

 

결국 

DB : 변경됨

영속석 컨텍스트 : 변경안됨

상태여서

 

벌크 쿼리후에 영속성컨텍스트를 초기화하고 새롭게 DB -> 영속성컨텍스트로 데이터를 가져와야한다.

 

JPA에서 영속성 컨텍스트 클리어

엄청 어려워 보이지만 방법은 간단하다

@Modifying(clearAutomatically = true)

로 설정하면 쿼리 이후 영속성 컨텍스트를 클리어 해준다.

@Modifying(clearAutomatically = false) 가 default이니 꼭 설정해주자

 

 

 

반응형
반응형

 

예전의 JPA에서의 페이징은 페이지 남은 페이지를 공식 적용해서 계산하고

어쩌고 저쩌고 ,, 하면서 복잡한 코드를 가지면서 페이징을 구현했었다.

 

하지만 스프링 데이터에서 JPA에 대해서 좋은 기능을 제공해준다.

 

스프링 데이터 JPA 페이징과 정렬 

페이징 정렬은 Sort를 통해서 정렬하게 되는데 이 기능이 

Pageable의 내부에 포합 되어있다.

Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함

 

대표적으로 페이징을 하는 방법인데

Page : count쿼리를 포함한다

-> 내가 몇번째 페이지에 있는지 같은 걸 계산해야하기 때문에 count쿼리를 사용해야한다.

Slice : Count 쿼리 없이 사용가능하다.

-> 내가 몇번째 페이지를 볼건지 선택해서 보는 것이 아니고 밑으로 내리면 다음 페이지가 자동으로 나오는 기능이다.

내부적으로 limit + 1 를 조회한다 .

 

즉, 10건의 데이터를 계속 조회하는 로직이 있다면 Page는 10건 , Slice는 11건을 조회해온다

 

위에서 Pageable은 인터페이스 이기 때문에 

실제 사용할때는 아래와 같이 

PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

 

PageRequest객체를 사용한다.

파라미터 1번째 : 현재페이지 (0부터 시작)

파라미터 2번째 : 조회할 데이터의 수

파라미터 3번째 : 정렬 할 기준 (누락 가능)

 

Page , Slice에 대해서 유용한 메소드

스프링 데이터 JPA에서는 여러가지의 페이징 메소드들을 제공해준다

ex) 현재페이지 , 전체 페이지 수 , 다음페이지 여부 등

이미 많은 기능들이 만들어져 있으니 가져다가 사용하면 된다.

 

Page를 사용할 때 countQuery를 한번에

Page는 자동으로 페이지가 안넘어가니 Page를 count 해야하는데

여기서 쿼리가 따로 나가고 조인하는 컬럼들이 따로 조회가 되면서 성능상 문제를 일으킨다

이를 한번에 쿼리를 날리지는 않지만 조인하는 컬럼들을 한번에 조회할 수는 있다.

 @Query(value = "select m from Member m",
        countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);

 

복잡한 SQL 에서 사용하며 데이터는 left join  , 카운터는 left join을 안해도 된다.

 

전체를 조회하는 count 쿼리 자체는 성능에 좋을리가 없기에 신경쓰는게 좋다

 

페이지 Entity -> DTO

이제는 당연한 이야기지만 

Entity를 api로 바로 노출하면 안된다.

그렇기에 조회한 데이터를 DTO로 변환해야하는데

Page<Member> page = memberRepository.findByAge(age , pageRequest);
Page<MemberDto> toMap = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));

 

다음과 같은 형태로 map을 사용하면 편리하게 DTO로 변환 할 수 있다.

 

느낀점

캡스톤 디자인을 할때 DTO에 2개의 Entity를 넣었어야해서 

 

이부분에 대해서 어떤 좋을 방법이 있을까 고민했엇는데

 

JPQL를 사용해서 쿼리를 직접 구현하니

DTO에 2개의 Entity 값을 넣는 방법에 대한 고민이 해결이 되었다.

반응형
반응형

 

 public interface MemberRepository extends JpaRepository<Member, Long> {
	 @Query("select m from Member m where m.username= :username and m.age = :age")
	 List<Member> findUser(@Param("username") String username, @Param("age") int age);
}

 

결국 스프링 데이터 JPA를 사용하면 우리는 Named Query를 등록해서 사용하는 것이 아닌

위와 같은 형태로 사용하는 것이 일반적이다.

물론 필요에 따라서 다른 형태로 변경되는건 당연한 일이지만.

순수한 JPA를 쓸때와 비교해보면 많이 달라진 모습이다. -> 편안,,

 

위와같이 파라미터를 받는 메소드를 쉽게 구현이 가능하고 find, By, Top등 관례가 있으니 찾아보면서 사용하자

 

 

Collection 파라미터 바인딩

 @Query("select m from Member m where m.username in :names")
 List<Member> findByNames(@Param("names") List<String> names);
 
 ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
 @Test 
 List<Member> result = memberRepository.findByNames(Arrays.asList("AAA" , "BBB"));

 

이런 형태로 컬렉션으로 조회를 할 수 있다

 

하지만 여기서 신경써야할 부분이 있는데

컬렉션은 결과가 없으면 빈 컬렉션을 반환하는데

 

단건조회는 null을 반환한다.

단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA에서는 

getSingleResult() 메소드를 호출하게되고

결과가 없거나 2개 이상이라면 Exception을 터트리게 되는데 

만약 조회 결과가 없어서 NoResultException예외가 터진다면 예외를 무시하고 스프링 데이터 JPA에서 null을 반환하게된다.

반응형
반응형

강의를 보던중 문득 이 생각이 들었다

 

    public void changeTeam(Team team){
        this.team = team;
        team.getMembers().add(this);
    }

Q : team.getMembers().add(this)를 해야할까?

만약 Member <-> Team 이 서로 연관관계 매핑이 되어있을 때

팀을 바꾸면 아래 코드에서 그냥 this.team = team 코드만 작성하면 되는거지 

굳이 team.getMembers().add(this)를 왜하는 거지 라는 생각이 들었다.

 

 

Team 에서 List<Member> 를 조회하면 DB에 쿼리를 날려서 Team 에 연관된 Member의 정보를 가져올텐데

라는 생각으로 이해하는데 어려움이 생겼던거였다.

A : 문제는 JPA 영속성 컨텍스트에서 발생한다.

하지만 문제는 JPA 영속성 컨텍스트에서 발생한다.

만약 영속성 컨텍스트가 비활성화 되어있거나 , 영속성 컨텍스트가 활성화 되어있지만 트랜잭션이 커밋이 되지 않은 상태라면

Team을 조회해도 DB가 아닌 영속성 컨텍스트에 캐싱된 데이터에서 가져오기 때문에 추가된 Member의 정보가 반영되지 않는다.

 

결국 DB에 반영되기전에 조회를 하면 반영되기전 데이터를 가져오기때문에 

양방향 연관관계 매핑이 되어 있다면 따로 List<Member>에서 add를 해줘야한다.

-> 한쪽에서 데이터를 추가했으면 다른 쪽 , 즉 양쪽 모두 추가해줘야한다.

 

이렇게 하면 메모리의 상태와 DB의 상태를 일치하게 유지할 수 있다.

 

 

 

반응형
반응형

 

페치 조인 과 일반 조인의 차이

join fetch 

페치 조인을 사용하면 연관된 엔티티도 함께 조회(즉시 로딩)

-> 따라서 컬렉션 엔티티로 조회해도 쿼리가 안날라감

-> 객체 그래프를 SQL 한번에 조회하는 개념

-> 결국 연관된 엔티티를 함께 조회한다   >> 페치 조인의 장점   N+1 문제 해결 

 

 

SQL

일반 조인은  SELECT절에 지정한 엔티티만 조회하게 된다

-> 따라서 컬렉션 엔티티로 조회하게 된다면 쿼리가 다시 날라감. >> 이게 바로 N+1 문제           

 

페치 조인의 특징 과 한계

query = "select t From Team t join fetch t.members m";

 

이러한 쿼리 문이 있을때

t.members m 같이 별칭을 사용하게 되면 안된다

 

왜냐하면 t.members안에는 team 과 관련된 members에 대한 정보들이 전부 들어있는데

이러한 members의 데이터를 건드리게 된다면 만약 10개의 데이터중 4개만 가져왔다면 

다음에 t.members 호출하게 되면 4건의 데이터만 나오게 된다

 

만약 members에 대한 정보를 조회해야하는 상황이 온다면

Team Entity 에서가 아닌 Member Ent

반응형

+ Recent posts