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
728x90

코드는 모두 깃허브에 있음.

일단 자바의 컬렉션 인터페이스들의 특징부터 나열한다.

컬렉션

  • Collection
    • 자바가 제공하는 최상위 컬렉현, Hibernate는 중복을 허용하고, 순서를 보장하지 않는다고 가정
  • Set
    • 중복을 허용하지 않고, 순서도 보장하지 않는다.
  • List
    • 순서가 있는 컬렉션아며 중복을 허용한다.
  • Map
    • Key, Value 구조로 되어있는 컬렉션이다.

JPA와 Collection

Hibernate는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 Hibernate

준비한 컬렉션으로 감싸서 사용한다.

다음 예시를 보자

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany
    private Collection<Member> members = new ArrayList<>();
}

@DataJpaTest
class TeamTest {

    @PersistenceUnit
    EntityManagerFactory emf;

    EntityManager em;

    EntityTransaction tx;

    @BeforeEach
    void setUp() {
        em = emf.createEntityManager();
        tx = em.getTransaction();
        tx.begin();
    }


    @Test
    void 컬렉션_테스트() {
        Team team = new Team();
        System.out.println(team.getMembers().getClass());
        em.persist(team);
        System.out.println(team.getMembers().getClass());
    }
}

테스트코드 지만 단순히 이 결과를 확인하기 위해서 콘솔 출력을 진행하였다.

결과는 이렇게 나온다.

image
{: text-center}

처음 객체를 포장할때는 Team 엔티티 클래스에서 명시한 ArrayList로 포장을 하는데 엔티티를 영속상태로 바꿔주는 순간 PersistentBag 으로 변경된다.

Hibernate는 컬렉션을 효율적으로 사용하려고 영속상태로 만들때 원본의 컬렉션을
감싼 내장 컬렉션을 생성하여 이 감싼 내장 컬렉션을 사용하도록 참조를 변경한다.

그렇기 때문에 컬렉션을 사용하려면 즉시 초기화를 해주고 사용하는걸 권장한다.

다음은 Hibernate의 내장 컬렉션들과 특징이다.

컬렉션 내장컬렉션 중복 순서
Collection, List PersistentBag O X
Set PersistentSet X X
List + @OrderColumn PersistentList O O

Collection, List

CollectionList는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고

왜❓ - 중복을 허용하기 때문

단순히 저장만 하면 된다. 그렇기 때문에 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.

Set

Set은 엔티티를 추가할 때 중복값을 확인하기 때문에 서로 비교를 해야한다.

그렇기 때문에 지연 로딩된 컬렉션을 초기화 한다.

List + @OrderColumn

@OrderColumn은 DB에 순서값을 저장해서 조회할 때 사용한다는 의미

순서가 있기에 DB에 순서값도 관리하는데

단점이 있어 사용하지 않는다고 한다.

순서값을 DB가 가지고 있기 때문에 하나를 지운다고 가정하면 삭제된 List의 번호에는 null이 저장된다.

NullPointerException우려

@OrderBy

책에서 나온것처럼 특정 칼럼에 @OrderBy를 주는 법도 있겠지만 이렇게 하지않고 대부분 Auditing 기능 오버라이드 하여 한다고 한다.

@Converter

컨버터는 단어 그대로 형 변환을 해주는 것이다.

예를들어 boolean 타입은 DB에 저장될 때 0과 1로 저장이 된다. 대신에 Y나 N으로 저장하고 싶다면

컨버터를 사용하면 된다.

@Converter
public class BooleanYNConverter implements AttributeConverter<Boolean, String> {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null && attribute) ? "Y" : "N";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return "Y".equals(dbData);
    }
}

이렇게 AttributeConverter를 구현해주고 @Converter를 명시해준다.

//방법 1
@Convert(converter = BooleanYNConverter.class, attributeName = "적용할 변수")
public class Test {
    //방법 2
    @Convert(converter = BooleanYNConverter.class)
    private boolean 변수명;
}

이렇게 있다. 그리고 추가로 모든 boolean에 대해서 적용을 시켜준다면

클래스최상단에 @Converter(autoApply = true)를 주면 된다.

리스너

JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트 처리 가능

image
{: text-center}

이벤트의 종류와 발생 시점은 위의 이미지와 같다.

  1. PostLoad : 엔티티가 영속성 컨텍스트에 조회된 후 또는 refresh 호출한 후(2차 캐시에 저장되어 있어도 호출).
  2. PrePersist : persist() 를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에. 식별자 생성전략을 사용한 (이하 @GeneratedValue) 경우 엔티티에 식별자는 아직 존재하지 않는다. 또한 새로운 인스턴스를 merge할 때.
  3. PreUpdate : flushcommit을 호출해서 엔티티를 DB에 수정하기 직전
  4. PreRemove : remove()를 호출해 엔티티를 영속성 컨텍스트에서 삭제하기 직전. 영속성 전이가 일어날 때, orphanRemoval(고아객체 관련)에 대해선 flushcommit시에
  5. PostPersist : flushcommit을 호출해서 엔티티를 DB에 저장한 직후 호출. 식별자 항상 존재함. 생성전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출한 직후 바로 호출.
  6. PostUpdate : flushcommit을 호출해서 엔티티를 DB에 수정한 직후
  7. PostRemove : flushcommit을 호출 엔티티를 DB에 삭제한 직후

이벤트 적용위치

적용 위치는 3가지이다.

  • 엔티티에 직접 적용
  • 별도의 리스너 등록
  • 기본 리스너 사용

엔티티에 직접 적용

@Entity
public class Entity {
    @Id @GeneratedValue
    private Long id;

    ...

    //아래로 쭉 구현
    @PrePersist
    public void prePersist() {
        ...
    }

    @PostPersist
    public void postPersist() {
        ...
    }
    ...

}

별도의 리스너 등록

이거는 JPA Auditing 생각해보면 될거같다. 결국 AuditingEntityListener 이 리스너도 안에 어노테이션으로 아래와 같이 구현되어있다.

@Configurable
public class AuditingEntityListener {

    private @Nullable ObjectFactory<AuditingHandler> handler;

    public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {

        Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
        this.handler = auditingHandler;
    }

    @PrePersist
    public void touchForCreate(Object target) {

        Assert.notNull(target, "Entity must not be null!");

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markCreated(target);
            }
        }
    }

    @PreUpdate
    public void touchForUpdate(Object target) {

        Assert.notNull(target, "Entity must not be null!");

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markModified(target);
            }
        }
    }
}

여러개의 리스너를 등록했을 때 호출순서는

  1. 기본 리스너
  2. 부모 클래스 리스너
  3. 리스너
  4. 엔티티

와 같다.

엔티티 그래프

엔티티 그래프는 엔티티를 조회하는 시점에 연관된 엔티티들을 함께 조회하는 기능이다.
Named 엔티티 그래프는 Named쿼리 자체의 빈도수가 낮기때문에 다루지 않겠다.

EntityGraph<Team> graph = em.createEntityGraph(Team.class);

graph.addAttributeNodes("속성");

JPAQuery<Emp> query = queryFactory.selectFrom(Q클래스).where(조건);

query = query.setHint("javax.persistence.fetchgraph", graph);

query.fetchOne();

이렇게 엔티티 그래프를 정의하고 Hint로 그래프를 넣어주면 되는 방식이다.

정리

엔티티 그래프는 항상 조회하는 엔티티의 ROOT경로에서 시작해야 한다.

만약 Member엔티티에 Team이 포함되어 있다면 Member조회 후 Team으로 가야되는데 역으로 갈 수는 없다.

영속성 컨텍스트에 엔티티가 이미 로딩되어 있다면 엔티티 그래프 적용 ❌

fetchgraph와 loadgraph의 차이는 loadgraph는 엔티티 그래프의 설정한 속성과 함께 글로벌 페치전략이 FetchType.EAGER 인 관계들도 전부 포함해서 함께 조회한다.

728x90

'JPA' 카테고리의 다른 글

JPA template 이슈  (0) 2022.08.07
고급 주제와 성능 최적화 1  (0) 2022.08.06
준영속 상태의 지연로딩을 해결하는 방법  (0) 2022.08.06
JPA metamodel must not be empty!  (0) 2022.08.06
728x90

📌 SOLID 원칙

객체 지향적으로 설계하는데에 있어 기본이 되는 SOLID원칙에 대해서 알아보자.
SOLID는 각 원칙의 첫번째 글자를 따서 만든 것이다.

  • 단일 책임 원칙 (Single responsibility principle; SRP)
  • 개방-폐쇄 원칙 (Open-closed principle; OCP)
  • 리스코프 치환 원칙 (Liskov substitution principle; LSP)
  • 인터페이스 분리 원칙 (Interface segregation principle; ISP)
  • 의존 역전 원칙 (Dependency inversion principle; DIP)

이 원칙들이 서로 다른 내용이라 하기에는 밀접하게 연관되어 있으므로 한꺼번에 같이 이해해야 할 것이다.

📌 단일 책임 원칙

객체 지향은 책임을 객체에게 할당하는데에 있다. 객체로 존재하게 하는 이유가 책임인데

이 원칙은 원칙 이름에서도 알 수 있듯 책임과 관련되어 있다.

📢 클래스는 단 한 개의 책임을 가져야 한다.

하나의 클래스가 여러 책임을 갖게되면 책임마다의 이유가 발생하기 때문에 클래스 변경이 잦아질 수 있다.

그래서 한개의 이유로만 클래스가 변경되려면 하나의 책임만을 가져야 한다.

지금 진행하고있는 TDD강좌를 들으면서 SOLID원칙 그리고 디자인 패턴에 대해 많이 학습하는것 같다.

1-2-3 과정으로 진행되는 과정이 예를 들어 있다고 한다면

public class Process {
    public void one() {
        /// 1...
        String test = two();
        three(test);
    }


    public void two() {
        /// 2...
    }

    public void three(String test) {
        /// 3...
    }
}

이렇게 작업을 하게되면, 연쇄적으로 코드 수정을 해야할 수 밖에 없다.

이것은 코드를 분리만했지 절차지향과 다를게 없다.

여기서 만약 String타입의 인자가 아니라 다른타입으로 오게되면 twothree 모두 바꿔야한다.

극단적인 예시이지만, 같은 작업을 수행하는 것이라면 괜찮지만
데이터를 불러오고, 가공하고, 보여준다 라면 얘기는 다르다.

그러면 클래스마다 1개씩 잘라서 분리를 해주어야 한다.

그래서 최종적으로 동작하는 클래스에서 조립하는 느낌으로 구현을 한다고 생각하면 이해가 쉬울 듯 하다.

📌 개방-폐쇄 원칙

개방, 폐쇄 말만들으면 열고 닫는것.

확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다.

말이 이렇게만 보면 어렵다.

기능을 변경하거나 확장은 할수 있되, 그 기능을 사용하는 코드는 수정을 하지 않아야 된다.

이부분은 추상화와 관련이 깊은것 같다. 사용하는 코드를 수정하지 않아야한다 라는 원칙이기 때문에

인터페이스 또는 추상클래스를 사용했을 시에 구현체에 따라 다르게 동작할 수 있게 만듬으로써

변경 확장이 용이하며, 코드 수정은 하지 않게된다.

개방 폐쇄 원칙이 깨질때의 증상 1 - 다운 캐스팅

이것은 다운 캐스팅을 할 경우에 그런데, instanceof 로 타입 다운을 시켜 검증을 하면

발생하는 것인데

내 경우에는 볼링게임 여기서 사용했었다.

객체마다 다르게 동작할 수 있는 무언가가 있다면 그 메소드를 추상화하여 동작하게 만들어야한다.

개방 폐쇄 원칙이 깨질때의 증상 2 - if-else

public void draw() {
    if (index == 1) {
        // 1조건
    }else if (index == 2) {
        // 2조건
    }
    ...
}

위 코드처럼 조건이 하나씩 늘게되면 else if로 늘려서 추가할 경우이다.

draw에서 그냥 메소드를 통일하고

Pattern이라는 인터페이스를 생성하고 방식에따라 다른 로직을 구현하고 메서드는 통일시키는 식으로

추가를 해주자.

개방 폐쇄 원칙은 유연함에 대한 것

이 원칙은 변화가 예상되는 것들을 추상화해서 유연함을 얻도록 해줬다.

그러니까 추상화를 하지 않거나 아직 개념이 부족해 못한다면

개방-폐쇄 원칙을 지킬 수 없게 되어 기능 변경이나 확장을 어렵게 만든다는 것이다.

내가 퇴사했던 회사에서 딱 이렇게 개발을 했었다. 오히려 이게 쉽다면서... 난 아니었는데 😥

각설하고, 코드의 변화 요구가 발생하면 이 구현부를 추상화해서 지금의 이 원칙에 맞게 수정할 수 있는지

항상 생각하면서 개발해야겠다.

📌 리스코프 치환 원칙

윗부분의 개방폐쇄에선 추상화 그리고 다형성을 이용하여 구현되었다.

이번에 알아보는 이 리스코프 치환 원칙은 개방 폐쇄를 조력해주는(?) 다형성에 관한 원칙이다.

상위 타입의 객체를 하위 타입의 객체로 치환하여도

상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

이거는 예제를 따로 개발자가 반드시 정복해야할 객체지향과 디자인 패턴에서 인용하였다.

부모 클래스와 자식 클래스가 있다고 가정. 특정한 메서드는 상위 타입인 부모클래스를 이용할 것이다.

public void someMethod(SuperClass sc) {
    sc.someMethod();
}

someMethod는 부모타입의 객체를 사용하고 있는데, 이 메소드에 하위타입 객체를 전달해도 someMethod

정상적으로 동작해야 한다. 이것이 바로 리스코프 치환 원칙이다.

중요❗❗❗ 이 원칙이 제대로 지켜지지 않는다면 개방-폐쇄역시 지켜지지 않는다.

개념적으로 상속 관계에 있는 두개의 클래스가 있더라도 막상 구현했을때는 상속 관계가 아닐 수도 있는 것들이 있다.

이럴 때는 상속이 아니라 각자 다른 타입으로 매칭을 시켜줘야한다.

📌 인터페이스 분리 원칙

인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

이 원칙또한 앞에서와 연관이 있는데 결국 용도에 맞게 인터페이스를 분리하는 것은

단일 책임 원칙과 연결이 된다. 하나의 타입에 여러 기능이 섞일 경우

한가지의 기능 변화로 인해 다른 기능이 영향을 받을 가능성이 높아진다.

그래서 인터페이스를 분리한다면 한 기능에 대한 변경의 리스크를 줄일 수 있게 된다.

그리고 인터페이스의 분리가 잘되어 있다면 즉, 단일 책임 원칙이 잘 지켜질때

구현클래스들의 재 사용성이 높아질 수 있기 때문에

결국 이 원칙은 인터페이스와 구현 클래스의 재사용성을 높인다는 효과를 가지게 된다.

원칙은 클라이언트에 대한 것

인터페이스 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하란 원칙이다.

클라이언트가 사용하는 기능들을 중심으로 인터페이스를 분리하여 클라이언트에서 발생하는 인터페이스

변경의 여파가 다른 클라이언트에 미치는 것을 최소화 할 수가 있다.

📌 의존 역전 원칙

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.

저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

고수준 모듈은 저수준 모듈을 조합해서 한 모듈을 만들어 놓은 형태이고

저수준 모듈은 도메인단위로 분리했을 때 상세한 행동 하나하나를 정의한 것을 말한다.

고수준 모듈이 저수준 모듈에 의존할 떄의 문제

내가 생각하는 이 SOLID 원칙. 어떤 법칙을 읽던간에 무조건 인터페이스가 생각나는게 당연한걸까❓

이것도 내가 생각하기엔 추상화가 답인것 같다.

구현하는 비즈니스 클래스가 여러개가 있다고 한다면 개방폐쇄의 예시처럼 조건일때 다른 클래스 로직을 불러와

실행하는 것이 될것이다.

이런 방식이 프로그램을 변경하는데 너무 어렵게 만든다. 이 저수준 모듈이 변경됨에 따라

고수준은 변경이 되지 않게끔 하려면 나오는 원칙이 의존 역전 원칙이다.

그러니까 이 의존 역전 원칙이 리스코프나 개방-폐쇄를 따르는 설계를 만들어주는 밑바탕이 되는 것으로 볼 수 있다.

의존 역전

마땅한 코드 예시가 없어서 책에서 한번더 가져다 쓴다.

public class FlowController {
    public void process() {
        //소스 코드에서 FileDataReader에 대한 의존 발생
        FileDataReader reader = new FileDataReader();
    }
}

public class FileDataReader implements ByteSource {
    ...
}

이런 구조가 있다면

소스코드의 의존은 아래와 같다.

image
{: text-center}

그런데 런타임에서 본다면
FlowController -> FileDataReader 가 되는것이다.

왜냐면 컨트롤러가 시작점이고 거기서 하나씩 로드해서 참조하기 때문에

이런 구조가 나온다.

왜?

이유는 그냥 단순하다. 절차지향으로 생각해보면 저 Controller를 동작시켜야 아래가 동작하지 않는가?

정리

지금까지 SOLID 원칙에 대해 알아보았다. 결국 이 원칙이 말하는 바는

정말 원초적으로 생각하고 따지자면 유지보수하기 좋은 코드를 만드는 밑바탕이다.

왜냐면 요구사항이 변화하는것에 맞춰 유연하게 변경하니까 가 가장크다.

요즘 자바 공부에 재미를 붙였다.

이렇게 하나하나 습득해서 위로 올라가고 싶다.

나는 아직 자바가 고프다 ㅋㅋㅋㅋㅋㅋ

이번 포스팅 마치도록 하겠다.

728x90

'Java' 카테고리의 다른 글

디자인 패턴 - Bridge Pattern  (0) 2022.08.07
Oauth 이슈  (0) 2022.08.07
연산자  (0) 2022.08.06
[Java] 데이터타입, 변수, 배열  (0) 2022.08.06
728x90

준영속 상태의 지연로딩을 해결하는 방법

JPA에서 항상 생각을 해야되는 것이 바로 영속상태, 영속성 컨텍스트위에 있는가? 를 생각해야된다.

예를 들어 우리는 DAO층에서 실제 DB와 통신을 보편적으로 진행하는데, 이쪽은 영속성 컨텍스트에 의해 관리가 되어
영속 상태를 유지한다.

하지만, Controller, View 이런 계층에서는 준영속 상태가 된다. 그래서 영속상태와 다르게 변경감지, 지연로딩이 동작하지 않게 된다.

지연로딩이 동작하지 않기 때문에 이때 지연 로딩을 시도하면 문제가 발생하는게 당연하다.

하이버네이트가 구현체라면 org.hibernate.LazyInitializationException이 발생한다.

이것을 해결하는 문제는 두가지가 있다.

  • 뷰가 필요한 엔티티를 미리 로딩
  • OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법

뷰가 필요한 엔티티를 미리 로딩

이름 그대로 영속성 컨텍스트가 살았을 때, 필요한 엔티티를 다 로딩하거나 초기화해서 반환하는 방법이다.

이미 다 로딩했기 떄문에, 지연 로딩이 발생할 걱정을 안해도 된다.

이 방법에서는 로딩해두는 방법에 따라 3가지로 나뉜다.

글로벌 페치 전략 수정

엔티티에서 fetch 타입을 변경하면 애플리케이션 전체에서 이 엔티티 객체를 로드할때 마다 해당 전략을 사용하므로 글로벌 페치 전략이라고 한다.

단점

사용하지 않는 엔티티까지 로딩
N + 1 문제 발생

JPQL을 사용할 때 문제가 발생하는데

Order와 Member가 다대일로 연결되어 있다고 가정한 후에
select o from Order o 를 사용해서 조회를 한다고 한다면
글로벌 페치 전략을 사용하지 않고 그냥 JPQL자체만 사용하기 때문에

  1. 일단 Order를 조회
  2. order 인스턴스들 생성
  3. member 페치 전략이 즉시로딩이므로 order조회되면 member도 조회
  4. 근데 영속성 컨텍스트에 없다? order엔티티 수만큼 계속 조회

이걸 해결하려고 나온것이 바로 아래 부분이다.

JPQL 페치 조인

여기선 join fetch를 사용하면 된다고 한다.

select o from Order o join fetch o.member

N + 1 자체의 문제를 해결해주는것은 좋다. 근데 이제 order만 조회하느냐, order에 연관된 member까지 조회하냐에 따라 메소드를 늘려야할 것이다. 이러면 내부적으로 논리적 의존관계 우려가 발생하기 때문에 잘 고려해서 사용해야 할 것이다.

강제 초기화

지연 로딩을 설정했을 때 연관된 엔티티는 프록시 객체이다.

이 가짜 객체는 실제 사용 시점에 초기화가 되는데 이것을 영속성이 살아있을 때 다 초기화를 하여 반환해준다면? 준영속에서도 사용이 가능하다.

Facade 계층 추가

뷰를 위한 프록시 초기화 담당 계층

서비스와 컨트롤러를 분리해서 그 계층 사이의 의존성을 한번 더 분리해주는 것이라고 생각하면 편하다.

Controller - Facade - Service 이렇게 말이다.

프록시를 초기화하려면 영속성 컨텍스트가 필요해서 Facade에서 트랜잭션을 시작해야 한다.

퍼사드 계층 역할 및 특징

  • 프리젠테이션, 도메인 모델 계층 간의 논리적 의존성 분리
  • 프리젠테이션 계층에서 필요로 하는 프록시 객체 초기화
  • 서비스 계층 호출하여 비즈니스 로직 실행
  • Repository 직접 호출해서 엔티티 탐색

이것도 근데 결국 프리젠테이션 계층에서의 준영속 상태 라는 것이 문제이기 때문에 고안해낸 것.

OSIV

OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어준다는 뜻.

뷰에서도 지연로딩이 가능❗️

프리젠테이션 계층에서 엔티티를 수정못하게 막는 방법은

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 래핑
  • DTO만 반환

들이 있다.

스프링 OSIV

True - 기본값

application.properties에 추가해주면 된다.

sping.jpa.open-in-view:true 

스크린샷 2021-08-22 오후 12 43 27

  • 장점

    • 그림과 같이 커넥션을 유지해서 영속성 컨텍스트 범위가 넓어진다.
    • 그래서 연관관계 LAZY fetch 전략을 서비스를 벗어난 곳에서 사용 가능하다.
  • 단점

    • 너무 오랜시간동안 DB 커넥션을 유지하여 리소스를 사용하기 때문에 실시간 트래픽이 중요한 애플리케이션에서 DB 커넥션이 모자랄 수 있음 (장애 발생)

False - 설정값

스크린샷 2021-08-22 오후 12 43 13

sping.jpa.open-in-view : false

DB 커넥션을 Transaction 내부까지만 유지
(트랜잭션은 Service에서 수행되니까 Service까지만 유지되는 것)

  • 장점
    • DB 커넥션 리소스의 효율적인 사용
    • 트랜잭션을 종료할 때 영속성 컨텍스트를 닫으면서 DB 커넥션을 반환
  • 단점
    • 모든 지연로딩을 트랜잭션 안에서 처리
    • 지연로딩에 관한 모든 로직을 Service / Repository 에서 해결해야 함

해결방안

  • Command와 Query를 분리하는 방법 (김영한님 선호)
    • Controller에서 지연로딩을 처리해야 할 때 Query용 Service를 만드는 것
  • OrderService
    • 핵심 비즈니스 로직
  • OrderQueryService
    • 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션)

결론

결국 OSIV는 DB 커넥션 리소스에 대한 효율적인 사용과 관련된 전략이다.

OSIV 실무 TIP

실시간 API 고객 서비스를 해야한다면 OSIV false설정

ADMIN 처럼 커넥션이 많지 않은 곳 OSIV true 설정

728x90

'JPA' 카테고리의 다른 글

고급 주제와 성능 최적화 1  (0) 2022.08.06
컬렉션과 부가기능  (0) 2022.08.06
JPA metamodel must not be empty!  (0) 2022.08.06
[JPA] findAll, findById 차이  (0) 2022.08.06

+ Recent posts