728x90

Querydsl

지금 현재 리팩토링을 진행중인 프로젝트가 있는데,

내가 스터디를 했던 JPA를 적용시켜 Mybatis에서 JPA로 이전작업을 하는 중에

생각했던것 보다 QueryDSL을 더 많이 사용하는 것 같다.

많은 분들이 올리신 JPA와 같이 쓰는 QueryDSL은 별도의 support클래스를 만들어서

할당하는 방식으로 사용했다.

나는 그런데 조금 다르게 설정을 했다.

커스텀 인터페이스 구성

공식문서의 4.6.1챕터 에서 보면 Querydsl을 같이 사용하려면

해당 엔티티 Repository인터페이스에 다중상속으로 RepositorySupport 인터페이스를 상속해주고

RepositorySupportImpl에서 Support 인터페이스를 구현해주었다.

이렇게 말이다.

@Repository
public interface UserRepository extends JpaRepository<User, Long>, UserRepositorySupport {

}
public interface UserRepositorySupport {
}

public class UserRepositorySupportImpl implements UserRepositorySupport {

}

이렇게 인터페이스를 상속받아주게 되면 기본이 Impl인데 상속받아주게 되면

커스텀 Repository가 등록이 된다.

그리고 JpaRepository를 구현하고 있는 SimpleJpaRepository에는 @Repository 어노테이션이

붙어있으므로 자연스럽게 전부 어노테이션은 달지 않아줘도 된다.

그리고 이제 저 SupportImpl 에는 Querydsl 세팅하면서 등록해준 빈을 생성자 주입으로 할당받아

사용하면 되겠다.

Querydsl 문법

일단 사실 부딪히면서 생긴 에로사항들이 꽤 나왔다.

음 단순한 이론책이 아니라, 실습을 해서 몸으로 깨달아야 하는 지식은

책보면서 그냥 이론만 학습해서는 상당히 휘발성인 지식이 되어버린다.

몸으로 깨달으면서 이렇게 포스팅으로 정리까지 한다? 아주 👍

조금 찾아봤던 문법들을 정리해보겠다.

add, subtract

일단 update 과정에서 조회수를 +1 해야하는 로직을 예로 들어보겠다.

UPDATE board SET read_cnt = read_cnt + 1 where board_no = ?

이렇게 될 것이다.

이걸 근데 어떻게 해줄까 고민을 했었다.

그냥 수식에 + 1을 해주려고 로직에 넣었더니 에러가 발생했다. 😭

jpaQueryFactory.update(board)
            .set(board.readCnt, board.readCnt.add(1)) // + 1의 경우
            .set(board.readCnt, board.readCnt.subtract(1)) // - 1 의 경우
            .where(board.id.eq(id))
            .execute();

두가지를 동시에 적었지만 하나만 사용했었다.

상당히 간단했던 에로사항인데 +1을 해야한다는 생각만 있어서 깊어지게 됐던것 같다.

이건 너무 간단하고 그리고 단순한 add()subtract() 로 해결했다.

진행했던 스터디분들에게도 여쭤봤었다.

감사합니다 ㅠㅠ 🙏

스크린샷 2021-09-17 오후 12 54 34

너무 간단해서 이 방식은 여기서 끝내도록 하겠다.

like, contains

검색기능을 사용하기 위해서 sql문에선 like를 사용했다.

여기서부터 이제 문법이란 문법은 다 **Expression 에 들어있는 것을 확인했다.

like, contains, startswith, endswith 전부 StringExpression

포함된 메소드들 이었다.

발견했던 것은 like는 해당하는 문자열이랑 같은 것을 뽑아주었고,

contains는 단어의 의미대로 앞뒤로 %를 붙여준 것과 같아서 나는 contains를 사용했다.

startswith는 특정 문자로 시작하는 것,

endswith는 특정문자로 끝나는 쿼리문을 만들어준다. 👍

public class NewBookRepositorySupportImpl implements NewBookReposirotySupport {
  public List<NewBook> selectGenre(String nbGenre) {
          return jpaQueryFactory.selectFrom(newBook)
              .where(newBook.nbGenre.contains(nbGenre))
              .fetch();
  }
}

max

최고 수치값을 쓸때는 단순하게 max를 써주면 되는데 왜 이 max 를 여기에 포함시켰냐 라고 묻는다면

내 경우에는 맥스 수치값을 뽑아내는게 아니라 맥스에 해당하는 컬럼을 조회했어야 했다.

select * from newbook where nb_scount = (select max(nb_scount) from newbook);

요런 형식의 sql문이었다.

자세히 들여다보니 maxNumberExpression에 있었다.

근데 저 괄호에 다시 select를 하려면 JPAExpressions 에서 JPQLQuery 인스턴스를 제공해주는데

그 클래스에서 쿼리를 새로 만들어주어야 했다.

아래는 클래스의 일부분이다.

스크린샷 2021-09-17 오후 1 09 01

이런식으로 쿼리를 만들어 줄 수 있었기 때문에

@Override
public NewBook selectBestSeller() {
    return jpaQueryFactory.selectFrom(newBook)
        .where(newBook.nbScount
            .eq(JPAExpressions.select(newBook.nbScount.max())
                .from(newBook)))
        .fetchOne();
}

위의 소스처럼 eq()안에 JPQL쿼리를 넣어서 만들어 주었다.

일단 이런 문법 자체를 잘 파악해서 필요한걸 매칭을 재빠르게 해주려면 필요한 것은

DB SQL문을 잘 알고 있어야한다.

정리

아직 더 큰 이슈사항은 만나지 않아서 여기까지 정리하도록 하겠다.

한가지 의문점은 원래 QueryDSL이 많아지는가? 에 대한 개인적인 생각이다.

기초적인 CRUD와 쿼리메소드가 아무리 강력하다고 한들 상세하게 조회하는 쿼리들에 대해선

QueryDSL이 빠질 수는 없으니까 말이다.

현재 단위테스트 진행은 이런 자잘한 이슈들을 겪으면서 한 테이블씩 천천히 진도를 나가는 중이다.

더 나아가서 서비스, 컨트롤러 등등 단위 테스트를 넣을 예정이며 통합테스트도 고려해야겠다.

728x90

'Java' 카테고리의 다른 글

변수  (0) 2022.08.07
상태 패턴 적용  (0) 2022.08.07
디자인 패턴 - Bridge Pattern  (0) 2022.08.07
Oauth 이슈  (0) 2022.08.07
728x90

모든 코드는 깃허브에 있다.

Bridge 패턴이란?

브릿지 패턴은 구현부에서 추상층을 분리하여 각자 독립적으로 변형 및 확장이 가능하도록 만드는 패턴이다.

이렇게 보면 너무 추상적인 문장이었다.

기능과 구현에 대해 두개의 별도 클래스로 구현한다.

Bridge의 뜻은 다리이다. 다리가 떨어진 장소를 연결하듯이 Bridge 패턴도 두 장소를 연결하는 역할을 한다고 보면된다.
이것이 Bridge 패턴의 핵심이다.
다리역할을 해주는 두 곳은 기능 클래스구현 클래스 이다.

두가지 경우로 나눠서 생각해보자.

  • 새로운 기능을 추가하고 싶은 경우
  • 새로운 구현을 추가하고 싶은 경우

어떤 새로운 클래스 ex가 있다고 예를 들자.

이 ex 클래스에 새로운 기능(=새로운 메소드)를 추가하고 싶을 때 어떻게 할것인가?

여기서 ex를 상속한 하위 클래스에 exChild 클래스를 만든다.
여기서 작은 클래스 계층이 생성된다.

ex ← exChild

이건 기능을 추가하기 위해 만들어진 계층이다.
이렇게 되면, 상위 클래스는 기본 기능은 가지고 있고, 자식 클래스에서는 새로운 기능을 추가할 수가 있게된다.

이러한 클래스 계층을 기능 클래스 계층 이라고 한다.

추상클래스는 메소드들을 추상 메소드로 선언하고 인터페이스를 정한다.
그리고 하위 클래스에서는 그 추상 메소드를 실제로 구현하는 역할을 한다.

이와같이 상위 클래스와 하위 클래스의 역할 분담에 의해 부품으로서의 가치가 높은 클래스를 만들 수 있는데, 여기서 두번째의 경우가 나온다.

상위 클래스는 추상 메소드에 의해 인터페이스를 규정
하위 클래스는 구체적인 메소드에 의해 그 인터페이스를 구현

이러한 클래스 계층을 구현 클래스 계층 이라고 한다.

새로운 구현을 만들기 위해선 추상클래스의 자식 클래스를 만들어 추상 메소드를 Override 하여 구현하면 된다.

기능 추가 or 구현 추가 선택

클래스 계층이 하나만 있다면 기능 클래스와 구현 클래스가 하나의 계층구조 안에서 섞이기 마련이다.

그래서 기능과 구현의 계층을 독립된 클래스 계층으로 분리하고 그 분리한 두 개의 클래스를 다리를 놓아 연결해주는것이 Bridge 패턴이다.

클래스 다이어그램

image

클래스 다이어그램으로 보면 다음과 같다.

  • Abstraction
    • 추상화 개념의 부모 클래스, 구현에 대한 참조자를 관리한다.
  • RefinedAbstraction
    • 추상화 개념의 확장된 기능을 정의한다.
  • Implementor
    • 인터페이스로써 구현 클래스에 대한 선언을 제공함.
    • ImplementorAbstraction의 메소드 이름은 다를 수가 있다.
  • ConcreteImplementor
    • Implementor에 선언된 기능을 구현하는 콘크리트 클래스이다.

클래스 다이어그램을 가만히 보고있으면 RefinedAbstraction 이 여러개라면,

추상 팩토리메소드가 구현이 될것 같은 느낌이다.

결국 Operation 인터페이스에 따라 또 정의된 Abstraction을 입맛에 맞게 끼워넣어

해당 기능을 사용하는 구조를 만들 수가 있다고 본다.

저 브릿지라는 개념을 생성하고 합성하는 것이 추상 팩토리 메소드 패턴과 관련이 있는것 같고

어댑터 패턴과도 관련이 있는것 같다.

아래는 예전에 구현했던 예제이고

최신으로 구현한 것은 깃허브에 있다.

Display.java

//기능 클래스 계층
public class Display {
    //이 필드가 두 클래스 계층의 다리역할을 한다.
    private DisplayImpl impl;

    public Display(DisplayImpl impl){
        this.impl = impl;
    }

    public void open(){
        impl.rawOpen();
    }

    public void print(){
        impl.rawPrint();
    }

    public void close(){
        impl.rawClose();
    }

    public final void display(){
        open();
        print();
        close();
    }

}

이 클래스는 기능 클래스 최상에 있는 클래스이다. DisplayImpl 필드는 Display 클래스의 구현을 담당하는 인스턴스이다.

이 Impl 필드가 Bridge가 되는것이다.

CountDisplay.java

//기능클래스
public class CountDisplay extends Display{

    public CountDisplay(DisplayImpl impl) {
        super(impl);
    }

    public void multiDisplay(int times){
        open();
        IntStream.range(0, times).forEach(i -> print()); //times 회수만큼 반복하여 표시
        close();
    }
}

Display에 times의 횟수만큼 표시하라는 기능을 추가한 기능 클래스이다.

DisplayImpl.java

//구현 클래스
public abstract class DisplayImpl {
    public abstract void rawOpen();
    public abstract void rawPrint();
    public abstract void rawClose();
}

추상클래스이며 처음,중간,끝에 해당하는 open,print,close 에 대응하여 처리된다.

StringDisplayImpl.java

public class StringDisplayImpl extends DisplayImpl{

    private final String string;
    private final int width;

    public StringDisplayImpl(String string){
        this.string = string;
        this.width = string.getBytes().length;
    }

    @Override
    public void rawOpen() {
        printLine();
    }

    @Override
    public void rawPrint() {
        System.out.println("|" + string + "|");
    }

    @Override
    public void rawClose() {
        printLine();
    }

    private void printLine() {
        System.out.print("+");
        //for문 stream형식 한번 sout 실행될때 '-' 출력
        IntStream.range(0, width).mapToObj(i -> "-").forEach(System.out::print);
        System.out.println("+");
    }
}

DisplayImpl을 상속받은 구현 클래스이다. 문자열을 표시하는 클래스 임과 동시에 DisplayImpl의 하위클래스로써 Override한 rawOpen, rawPrint, rawClose를 구현하였다.

Main.java

public class Main {
    public static void main(String[] args) {
        Display d1 = new Display(new StringDisplayImpl("Hello, Korea"));
        Display d2 = new Display(new StringDisplayImpl("Hello, world"));

        CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, universe"));

        d1.display();
        d2.display();
        d3.display();

        d3.multiDisplay(5);
    }
}

Main클래스에서는 전체 클래스를 조합하여 문자열을 표시한다.
d1,d2,d3 전부 Display의 일종이므로 display 메소드를 호출할 수 있으며, d3은 CountDisplay에 구현된 multiDisplay메소드를 호출 가능하다. Display 인스턴스 안에 StringDisplayImpl 클래스의 인스턴스가 구현을 담당한다.

Bridge 패턴의 핵심

이 패턴의 핵심은 기능 클래스구현 클래스를 분리하는 것이다.
이 두 클래스 계층을 분리해두면 각각의 클래스 계층을 독립적으로 확장이 가능하다.

728x90

'Java' 카테고리의 다른 글

상태 패턴 적용  (0) 2022.08.07
프로젝트 리팩토링  (0) 2022.08.07
Oauth 이슈  (0) 2022.08.07
SOLID 원칙  (0) 2022.08.06
728x90

비교적 오랜만에 글을 업로드 하는 것 같다.

사이드 프로젝트와 여러가지 업무에 매진해서 블로그 포스팅할 새도 없이 쭉 지나갔다.

모든 코드는 깃허브에 있다.

이번 포스팅에서는 Encyption 즉, 암호화에 대해서 다뤄보겠습니다.

서론

우선 현재 저희가 쓰고있는 암호화에는 각종 Open Api 클라이언트 ID와 비밀번호에 대해 암호화를 적용하였습니다.

스크린샷 2021-09-12 오후 8 37 13

예시로 하나의 암호화된 아이디와 패스워드를 가져왔습니다.

Github 또는 그 외의 공개 장소에 프로젝트 관리를 하다보면 DB 패스워드 등 보안에 민감한 정보들이 노출되기 쉽습니다.

그렇기 때문에 이런 민감한 정보들은 암호화를 해주어야 합니다.

그냥 명시적으로 적어두게 되면 누군가가 이 아이디와 암호를 보고 같이 사용할 수 있기 때문입니다.

Jasypt 라이브러리

일단 모든 설명은 공식문서와 깃허브에서 가져왔음을 알립니다.

Encrption이라는 키워드로 검색했을 때 Jasypt 라는 라이브러리가 자주 등장했습니다.

Jasypt는 자바에서 암호화를 쉽게 할 수 있도록 도와주는 라이브러리였습니다.

나는 Spring에서 프로젝트가 실행될 때 암호화를 해제하는걸 원했는데 딱 원하던 내용을 찾을 수 있었습니다.

@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
    PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
    SimpleStringPBEConfig config = new SimpleStringPBEConfig();
    config.setPassword("password");
    config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
    config.setKeyObtentionIterations("1000");
    config.setPoolSize("1");
    config.setProviderName("SunJCE");
    config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
    config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
    config.setStringOutputType("base64");
    encryptor.setConfig(config);
    return encryptor;
}

공식 문서에는 이렇게 Bean을 세팅하여 주입하는 식으로 설명이 되어있었습니다.

스크린샷 2021-09-12 오후 8 49 17

이 암호화 Configuration 설명을 보시면 키들이 properties 형식으로 작성이 되어있기에 저희 프로젝트의

yaml 형식으로 바꾸어

jasypt:
  encryptor:
    password: "비밀번호"

를 만들고 타고 들어갔을때의 내부 구현 클래스의 사진을 가져왔습니다.

스크린샷 2021-09-12 오후 8 47 51

properties의 설명은 당연히 jasypt.encryptor의 하위에 소스들을 읽어 들여서
세팅을 해주게 되어있습니다.

이미지를 부분만 캡쳐하여 가져왔기 때문에 보이지는 않지만,

조회해본 결과 password 외에는 전부 기본값이 설정이 되어 적용되어 있음을 확인하였습니다.

ENC() 문자열로 감싸준 이유는 암호화문을 ENC 괄호안으로 넣어주었을 경우

설정해준 jasypt.encryptor.password의 값을 읽은 후 복호화를 진행해줍니다.

저는 아래와 같이 테스트코드를 작성하여 암호를 설정해 암호화를 진행했습니다.

public class JasyptTest {
    @Test
    void jasyptTest() {
        StandardPBEStringEncryptor pbeEnc = new StandardPBEStringEncryptor();
        pbeEnc.setAlgorithm("PBEWithMD5AndDES");
        pbeEnc.setPassword("사용할 암호");

        String clientId = pbeEnc.encrypt("암호화 하려는 아이디");
        String password = pbeEnc.encrypt("암호화 하려는 비밀번호");

        System.out.println(clientId);
        System.out.println(password);
    }
}

고민사항

여기서 암호화를 하는 과정은 알게 되었습니다.

여기서 했던 고민은 결국 이 비밀번호도 어딘가에 저장해야 하는데

어떻게 저장해야 할까?

가 관건이었습니다.

회의를 통해 나온 결론은 모든 암호화는 지금처럼 적용하되, 해당 암호에 대한 설정은

각자의 컴퓨터의 환경변수로 잡아주자 라는 결론으로 모아졌습니다.

그래서 저희는 환경변수로 잡아주었습니다.

저는 맥의 환경이었으므로 이렇게 진행하였습니다.

cd ~
vi .profile/

# vi 에디터 사용하여 쉘 스크립트 작성으로
export JASYPT_PASSWORD="패스워드"
:wq

source ~/.profile

를 해주었습니다.

이 환경변수를 잡는 과정에서 상당히 많은 이슈가 발생했었는데요 😭

일단 저희는 깃허브 CI, CD를 사용하고 있기 때문에

push를 하게 되면 이후의 깃허브 가상 도커 컨테이너에서 빌드를 진행하여 저 부분에 대해서는 환경변수를 넣기 애매했습니다.

그리고

java build -DJASYPT_PASSWORD
java test -DJASYPT_PASSWORD

이런 명령어들 마다 계속 값 세팅이 달라졌기 때문에 하나가 성공하면 하나가 실패하는 그런 케이스가 지속이 되었습니다.

깃허브에서는 어떻게 환경변수를 넣을까 공식문서를 찾아본 결과

env:
      JASYPT_PASSWORD: ${ secrets.JASYPT_PASSWORD } # 중괄호 2개 사용해야함

CI,CD yaml파일에 다음과 같이 추가를 해주면 환경변수로 잡아줄 수가 있었습니다.

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      JASYPT_PASSWORD: ${ secrets.JASYPT_PASSWORD } # 중괄호 2개 사용해야함

secrets는 깃허브 Action 설정에서 전역으로 JASYPT_PASSWORD 라는 키에 저희 암호를 넣어주고 ${} 형식으로 불러올 수가 있습니다.

이렇게 하고 명령어를 써주어 암호화를 진행할 수 있게 되었습니다.

이 부분이 깊게는 알아서 쓰는것은 아니고 단순히 암호화를 하여 소스를 가리는 목적으로만 사용해서

이정도 깊이정도만 학습해도 별 문제없이 쓸수 있을정도의 라이브러리 였기 떄문에 여기까지만

알아보도록 하겠습니다.

정리

팀 프로젝트를 협업하면서 이번 부분에서 보안에 굉장히 많은 시간을 제가 투자하고 있는것 같습니다.

처음 적용 시켜보지만 디버깅과 깊게 조사하면서 배우니까 확실히 제껄로 만들어가는 느낌이 굉장히 좋습니다.

728x90

'Spring' 카테고리의 다른 글

Validaion  (0) 2022.08.09
Filter, Interceptor 정리  (0) 2022.08.07
Spring -> Spring Boot 마이그레이션 2  (0) 2022.08.06
Spring Data JPA  (0) 2022.08.06
728x90

1달 1일 1커밋 회고

늦었지만 1일 1커밋 한달의 회고 후기에 대해 남긴다.

일단 생활 패턴이 바뀌었고, 스스로 학습하면서 재미를 붙이게 된것도 맞다.

하루에 1커밋을 하자는 강박관념이 박히게 되서 커밋을 한게 아니라 그냥 기초 공부부터 다시 하려고 생각하니까

저절로 1일1커밋을 하게되었다. 토이 프로젝트와 블로그, 그리고 TDD강의로 인해서

습관이 들여진게 너무 좋다.

토이 프로젝트

토이프로젝트에 참여하게 되면서 협업에 대한 경험을 무지많이 하게 되었다.

git에 대해서 조금 더 고급적이게 브랜치를 활용하는 경험을 프로젝트를 통해 하게 되었다.

회사에선 혼자 백엔드를 맡다보니까 이런 부분은 경험할 수가 없었다.

정리

결국 1일 1커밋도 내 마음먹기에 달려있다고 생각한다. 지금 이렇게 습관을 들인것을 지속적으로 유지하고 싶어졌다.

왜?

저 잔디에서 오는 공부의 뿌듯함과 더불어 하루를 빠지게 된다면 치아가 1개 빠진것 처럼 비어보이는게 크다.

지금 내가 공부하는 방법을 터득해서인진 모르겠지만, 학원에서 배운건 나에게 많은 도움은 되지 못했다는걸 느낀다.

단지 그때보다 실력, 응용력이 높아져서인지, 아니면 교육과정 자체에서의 문제였는지 사실 가늠하지는 못하지만

확실한건 지금이 기초베이스가 더 다져지고 있다.

아무튼 이 습관으로 짧게는 6개월 그리고 1년, 2년 점점 1일1커밋 기간을 늘려가고 싶다.

1달 1일 1커밋 회고

늦었지만 1일 1커밋 한달의 회고 후기에 대해 남긴다.

일단 생활 패턴이 바뀌었고, 스스로 학습하면서 재미를 붙이게 된것도 맞다.

하루에 1커밋을 하자는 강박관념이 박히게 되서 커밋을 한게 아니라 그냥 기초 공부부터 다시 하려고 생각하니까

저절로 1일1커밋을 하게되었다. 토이 프로젝트와 블로그, 그리고 TDD강의로 인해서

습관이 들여진게 너무 좋다.

토이 프로젝트

토이프로젝트에 참여하게 되면서 협업에 대한 경험을 무지많이 하게 되었다.

git에 대해서 조금 더 고급적이게 브랜치를 활용하는 경험을 프로젝트를 통해 하게 되었다.

회사에선 혼자 백엔드를 맡다보니까 이런 부분은 경험할 수가 없었다.

정리

결국 1일 1커밋도 내 마음먹기에 달려있다고 생각한다. 지금 이렇게 습관을 들인것을 지속적으로 유지하고 싶어졌다.

왜?

저 잔디에서 오는 공부의 뿌듯함과 더불어 하루를 빠지게 된다면 치아가 1개 빠진것 처럼 비어보이는게 크다.

지금 내가 공부하는 방법을 터득해서인진 모르겠지만, 학원에서 배운건 나에게 많은 도움은 되지 못했다는걸 느낀다.

단지 그때보다 실력, 응용력이 높아져서인지, 아니면 교육과정 자체에서의 문제였는지 사실 가늠하지는 못하지만

확실한건 지금이 기초베이스가 더 다져지고 있다.

아무튼 이 습관으로 짧게는 6개월 그리고 1년, 2년 점점 1일1커밋 기간을 늘려가고 싶다.

728x90

'Diary' 카테고리의 다른 글

블로그를 옮기고 최신 근황  (0) 2022.08.13
업무 리팩토링에 대한 회고  (0) 2022.08.10
업무 회고  (0) 2022.08.07
TDD Clean Code with Java 12기 - 1주차  (0) 2022.08.05
728x90

진행중인 사이드 프로젝트에서 생겼던 이슈를 정리합니다.

모든 코드는 깃허브에 있습니다.

스터디 위키 정리중에창훈님께서 스프링 시큐리티에 대한 정보를 공유해주셨습니다.

그안에 포함되어있는 스프링 시큐리티 Oauth2에 대해서 포스팅을 해볼 것입니다.

Oauth 란?

OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준입니다. 이 매커니즘은 여러 기업들에 의해 사용되는데, 저희 서비스로 생각한다면 구글, 카카오, 애플, 깃허브, 페이스북 들이 있으며 사용자들이 타사 애플리케이션이나 웹사이트의 계정에 관한 정보를 공유할 수 있게 허용해줍니다.

OAuth인증은 소비자와 서비스 제공자 사이에서 일어나는데 이 인증 과정은 다음과 같습니다.

image

여기서 소비자는 내 서비스임을 명시하고

서비스 제공자는 구글 로 가정하겠습니다.

  1. 소비자가 서비스제공자에게 요청토큰을 요청합니다.
  2. 서비스제공자가 소비자에게 요청토큰을 발급해준다.
  3. 소비자가 사용자를 서비스제공자로 이동시킨다. 여기서 사용자 인증이 수행된다.
  4. 서비스제공자가 사용자를 소비자로 이동시킨다.
  5. 소비자가 접근토큰을 요청한다.
  6. 서비스제공자가 접근토큰을 발급한다.
  7. 발급된 접근토큰을 이용하여 소비자에서 사용자 정보에 접근한다.

인증

이 Oauth에 대한 인증은 기본적으로 Spring Security 내에 명시가 되어있습니다.

바로 CommonOAuth2Provider 라는 Enum 클래스인데요.

이 클래스를 사용해서 Bean을 등록하는것도 가능합니다.

public enum CommonOAuth2Provider {
GOOGLE {

        @Override
        public Builder getBuilder(String registrationId) {
            ClientRegistration.Builder builder = getBuilder(registrationId,
                    ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
            builder.scope("openid", "profile", "email");
            builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
            builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
            builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
            builder.issuerUri("https://accounts.google.com");
            builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
            builder.userNameAttributeName(IdTokenClaimNames.SUB);
            builder.clientName("Google");
            return builder;
        }

    },

    //....
}

하지만 제가 구현한 방식은 이 방법이 아니라 application.yaml에서 설정을 해주었습니다.

security:
    oauth2:
      client:
        registration:
          google:
            client-id: ENC(6/Ponyj/US/TvdVEZJfmcsPmvaXwKaeCWyudJyZDkNoTnloH+R4r0dR8MFFrA90SkJag3l4b67WQrIqR9Oz8Y6IK45M9ypW0DMxUGsOFc3rFijwDrAhK2Q==)
            client-secret: ENC(FoXXYeauW6EKov8iMfbE57X8cV9jEf4WSQ+FFmjnNIakgPv768fkng==)
            scope: profile, email

설정을 해주면 Oauth2Configurer를 통해 구현되게 되는데요.

ENC()jasypt 라는 보안으로 인한 암호화 모듈인데요 다른 글에서 포스팅을 하도록 하겠습니다. 😅

OAuth2LoginAuthenticationFilter에는 Default URI인 /login/oauth2/code/* 가 들어있습니다.

그래서 위의 yaml에서 redirect_uri를 잡아주지 않는다면 이 default를 통해 redirect 하게 됩니다.

이슈 사항

그래서 구글 api에도 /login/oauth2/code/google 로 설정해주었습니다.

저희 cors원칙을 설정해주기 위해서 리다이렉션 URI를 /api/v1/login/oauth2/google 과 같이

추가해주기 위해 yaml google에는 아래를 추가해주었습니다.

redirect-uri: '{baseUrl}/api/v1/login/oauth2/{registrationId}'

SecurityConfig의 일부입니다.

public class SecurityConfig {
@Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .httpBasic().disable()

                // =====일반 로그인 처리 로직 진행=====


                // Oauth 로그인 수행
                .oauth2Login(login ->
                        login
                                .userInfoEndpoint()
                                .userService(customOauth2UserService)
                )
    }
}

이렇게하고 구글 api설정도 바꿔주면 진행이 잘될거라고 생각했습니다.

디버깅을 통한 login 이라는 Oauth2LoginConfigurer의 일부입니다.

스크린샷 2021-09-02 오후 9 50 22
{: text-center}

스크린샷 2021-09-02 오후 10 10 41

login을 처리하는 url은 바뀌지않고 OAuth2LoginAuthenticationFilter의 default 값이 들어있는 것을 확인하였습니다.

위 코드에서 login 객체에 보면 프로세싱 url은 변함없이 기본값을 할당하고 있었습니다.

스크린샷 2021-09-02 오후 10 11 03

이유는 할당한 채로 제가 커스텀하여 할당하지 않았기 때문에 저 상태로 기본값이 저장되어 있던 것입니다. 😱

해결은

.oauth2Login(login -> 
        login.loginProcessingUrl("/api/v1/login/oauth2/*")
                                .userInfoEndpoint()
                                .userService(customOauth2UserService)
)

이렇게 프로세싱 url을 정해주고 다시 실행하니 정상적으로 실행 되는 것을 확인하였습니다. 👍

이렇게 해서 모든 Oauth 로그인이 돌아가는 형식으로 구성되어 있습니다.

스크린샷 2021-09-02 오후 10 07 56

그렇게 하여 ClientResistration에는 yaml에서 설정했던 id, secret, redirect-uri가 여기서 할당된것을 확인할 수 있습니다.

728x90

'Java' 카테고리의 다른 글

프로젝트 리팩토링  (0) 2022.08.07
디자인 패턴 - Bridge Pattern  (0) 2022.08.07
SOLID 원칙  (0) 2022.08.06
연산자  (0) 2022.08.06
728x90

스터디도 시작한지 3개월이 되었다. 시간 참빠른것 같다.

진도가 이제 15장을 둘로 쪼개고 16장 해도 9월에는 종료가 될것이다. 😂

예외처리와 엔티티 그리고 프록시에 대해 정리한다.

예외 처리

image{: text-center}

JPA는 그림과 같이 javax.persistence.PersistenceException의 자식 클래스이다.

그리고 이 예외 클래스는 RuntimeException의 자식이다.

JPA 예외는 모두 uncheck Exception이다.

JPA 표준예외

  • 트랜잭션 롤백을 표시하는 예외
  • 트랜잭션 롤백을 표시하지 않는 예외

서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존하는 것은 좋지 않다.
SOLID원칙 생각해보자. 이것이 예외에서도 마찬가지가 된다.

서비스에서 JPA의 예외를 그대로 사용한다면 JPA에게 의존하는것이 된다.

그래서 Spring은 이런 문제를 해결하려고 예외를 추상화해서 제공하였다.

방법

JPA 예외를 스프링 프레임워크에서 제공하는 추상화된 예외로 변경하려면 PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 등록해준다.

@Repository를 사용한 곳에 여기에 예외 변환 AOP를 적용해준다.

@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation {
    return new PersistenceExceptionTranslationPostProcessor();
}

트랜잭션 롤백 시 주의 사항

  • 트랜잭션을 롤백하여 DB의 데이터가 원래대로 복구되지만 영속성 컨텍스트까지 롤백이 된것은 아니다. 그래서 영속성 컨텍스트를 초기화 해준 후 사용해야 한다.
  • OSIV는 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 했기 때문에 자주 발생가능
    • 같은 엔티티에 여러 트랜잭션 발생 가능, 이렇게 사용하면 문제가 야기될 수 있다.
    • 넓게 설정해야한다면, 트랜잭션 롤백 시 영속성 컨텍스트를 초기화한다.

엔티티 비교

저장하는 em.persist()와 Spring data JPA의 findById()로 아이디값을 같은것을 비교하면 저장한 엔티티와 불러온 엔티티는 값만 같은 것이 아니라 인스턴스가 완전히 같다.
같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하기 때문이다.

  • 동일성
    • == 로 주소값을 따짐
  • 동등성
    • equals()로 값만 비교
  • DB동등성
    • @Id DB 식별자가 같다.

여지껏 내가 했던 테스트가 잘못될 수도 있었다. @Transactional 의 중요성 그리고 영속성 컨텍스트에 대해 계속 생각하며 Junit 테스트코드를 작성했어야 했다.

왜냐하면

@Test
@Transactional
void test() {
    Member member = new Member("홍길동");

    Member findMember = memberRepository.findById(1L).orElseThrow(new Member("테스트"));


    assertThat(member.getId()).isEqualTo(findMember.getId())
}

이렇게 짜주니까 이상적으로 동작했던것이었지 왜냐 ❓

엔티티를 영속화해야 DB 동등성 비교가 가능하다. 근데 만약 @Transactional이 각기 다르게 되어 동작을 할 때엔 이런 비교는 에러를 뱉게 된다.

프록시 심화

프록시는 원본 엔티티를 상속받아서 만들어지므로 엔티티를 사용하는 클라이언트는 엔티티가 프록시인지, 원본 엔티티인지 확인하면서 사용할 필요가 없다.

따라서 원본 엔티티를 사용하다가 지연 로딩으로 프록시로 변경되어도 다른 로직을 건들 필요가 없다.

이렇게하면 프록시를 먼저 조회했기 때문에 어차피 원본 엔티티를 상속받아 만들어진 것이기 때문이다.

영속성 컨텍스트와 프록시

작업 단위로 영속성 컨텍스트가 돌아가기 때문에 프록시로 조회하나 아니면 실제 엔티티를 조회하나

둘다 같은 객체를 반환해야 영속성 컨텍스트가 동일성을 보장하면서 운영할 수가 있다.

예를 들어

Member proxyMember = em.getReference(Member.class, "member");
Member findMember = em.find(Member.class, "member");

여기는 프록시 객체로 영속성 컨텍스트 동일성을 보장함

처음 프록시로 조회된 객체를 바로 반환시켜준다.

Member findMember = em.find(Member.class, "member");
Member proxyMember = em.getReference(Member.class, "member");

여기는 원본 엔티티 객체로 영속성 컨텍스트 동일성 보장.

이런것 까지 꼼꼼하게 생각 안하게 만들어준 JPA다.

클래스 타입비교

상속받아 만들어진 프록시 객체는 원본을 상속받은 객체이기 때문에 instanceof 로 타입을 비교해야 한다.

정리

전체적인 내용을 살펴보면 결국 영속성 컨텍스트가 얼마나 살아있는지에 대해 프록시과 원본 엔티티 동등성을 유지할 수 있는가가 주 내용이다.

작업단위를 잘 생각하고 영속성의 경계선을 파악하면 잘 사용할 수 있을것 같다. 지연로딩에 대해서 너무 간단하게 생각하지말고 부모자식으로 조회할 때의 프록시 객체를 생각하며 개발해야 한다.

728x90

'JPA' 카테고리의 다른 글

DataIntegrityViolationException에 대해서  (0) 2024.08.20
JPA template 이슈  (0) 2022.08.07
컬렉션과 부가기능  (0) 2022.08.06
준영속 상태의 지연로딩을 해결하는 방법  (0) 2022.08.06

+ Recent posts