728x90

여기서 나오는 모든 예제는 깃허브에 있다.

📌 변수

변수는 값을 저장하는 공간이다.

📌 변수란?


프로그래밍에서의 변수란, 값을 저장할 수 있는 메모리상의 공간이라고 한다.

이 공간에 저장된 값은 변할 수 있기 때문에, 변수라고 이름이 붙여졌다.

변수란, 단 하나의 값을 저장할 수 있는 메모리 공간

그렇기 때문에, 새로운 값을 저장하면 기존의 값은 날아가게 된다.

📌 변수의 선언과 초기화


📌 변수의 선언

변수의 선언 방법은

public class Class {
    int age; // age라는 이름의 변수 선언
    // int : 변수 타입
    // age : 변수 이름
}

변수 타입은 변수에 저장될 값이 어떤 타입인지를 지정해주는 것이다.

변수 이름 변수의 이름이다. 이 변수 이름 은 메모리 공간에 이름을 붙여주는 것이다.

int age; 메모리 주소지가 1이라고 가정했을 때,

int 타입의 age 변수를 1 주소지에 할당해라 가 되는 것이다.


📌 변수의 초기화

변수를 선언한 이후에는 변수를 사용할 수 있는데, 그전에 반드시 변수를 초기화 시켜주어야 한다.

메모리는 여러 프로그램이 공유하기 때문에, 다른 프로그램에 의해 그 메모리가 알 수 없는 값이 들어있을 수 있기 때문에

반드시 초기화를 해주어야 한다.

여기서 연산자가 나오는데 바로 대입 연산자(=) 이다.

이 대입연산자로 변수에게 값을 할당해줄 수 있다.

int age = 26 → 변수 age를 선언하고 26으로 초기화 시켰다.

같은 타입이라면

int a;
int b;
int x = 0;
int y = 0;

//같은 코드
int a, b;
int x = 0, y = 0;

하지만 코딩 컨벤션은 한줄에 하나를 할당하는게 대부분의 규칙처럼 정형화 되어있다.

이 부분에 대해선 협업하는 사람들에 따라 갈리겠지만 보통의 개발자들은 전자를 선호할 것이다.

변수의 초기화란, 변수를 사용하기 전에 처음으로 값을 저장하는 것

@Test
void test() {
    int year = 0;
    int age = 21;

    year = age + 2000; //변수 age + 2000
    age += 1; //age 저장된 값 1증가

    assertThat(year).isEqualTo(2021);
    assertThat(age).isEqualTo(22);
}

대입 연산자는 바로 수행되는 것이 아니라, 우변에 모든 계산이 끝난 후에 할당을 진행한다.

메서드 안에서 변수를 초기화하는 경우엔 명시적으로 int a = 0; 이렇게 초기화를 진행해주지만,

클래스 변수 즉, 전역변수는 int a; 이렇게 선언만 해주더라도 int 타입에 String을 넣는다거나

혹은 String 타입에 int를 넣는

참사를 방지하기 위해 컴파일러에서 아래에 출력한 0, null처럼 자동으로 값을 할당해 준다.


📌 두 변수의 값 교환하기

너무 유명한 swap 예제이다. CallByValue, CallByReference

조금 원초적으로 책에서는 설명되어있는데,

int a = 10;
int b = 20;

두 값을 서로 바꾸려고 할 때, b에 저장된 값을 a에 해버리면 둘다 20이 될 것이다.

그러기에 중간 변수를 하나 두어 스왑을 해주어야 한다.

int a = 10;
int b = 20;
int temp = 0;

temp = a;
a = b;
b = temp;

이렇게 해서 값을 바꿔주어야 한다.


📌 변수의 타입

값은 크게 문자, 숫자 두가지 부류로 나눌 수 있다.

숫자는 또 실수와 정수로 나뉜다.

이런 값의 종류에 따라서 값이 지정될 공간의 크기, 저장형식을 정의한 것이 자료형이다.

이 자료형에는 문자형(char),정수형(byte, short, int, long), 실수형(float, double) 등이 있다.

저장하려는 값의 특성에 따라 알맞은 자료형을 맞춰주면 된다.

📌 기본형과 참조형

  • 기본형
    • 변수는 값을 실제 값을 저장
    • boolean, char, byte, short, int, long, float, double
  • 참조형
    • 값이 저장되어 있는 주소를 값으로 가짐
    • 8개의 기본형을 제외한 나머지 타입

변수 타입에 대한 정리는 여기

📌 상수와 리터럴

상수(constant)는 말 그대로 변하지 않는 값. 그래서 변수 앞에 final을 붙여주면 된다.

final은 선언과 동시에 무조건 초기화를 해주어야하며, 그 이후엔 변경할 수 없다.

static 키워드도 같이 붙여주게 되는데,

붙이는 이유는 어떠한 객체에 final 변수가 하나 있다면 그 객체를 생성할 때마다,

final변수는 생기게 된다. 하지만 static 을 사용해서 메모리에 적재하게 두면 1개의 변수만이 여러개의 클래스에서 공유하게 되는 것이다.

이 상수를 쓰는 이유는 나는 2가지라고 생각한다.

첫째, 메서드안에 연산을 수행할 때, 하드 코딩 으로 숫자를 집어넣게 되면 무슨일을 하는 숫자인지 모를 수 있다.

그렇기에 상수로 떼어내어서 어떤 역할을 하는 변수인지 의미있게 이름을 준다.

둘째, 반복되는 숫자를 상수로 도출하면 반복코드를 제거할 수 있고, 들어가 있던 값을 상수만 바꿔주면

수정작업도 용이하기 때문이라고 본다.

📌 형변환

캐스팅(casting) 이라고도 부르며 서로 다른 타입간의 연산을 수행할 때,

같은 타입으로 변환시켜주는 것이 바로 형변환이다.

변수나 리터럴 앞에 변환하고자 하는 타입을 명시해주면 변환이 된다.

(타입)피연산자

기본형에서 boolean을 제외한 나머지 타입들은 서로 형변환이 가능하다.

하지만 기본형과 참조형간의 형 변환은 불가능하다.

그리고 기본형 타입에서도 작은 값을 큰값으로 그러니까 int형 변수를 long 자료형에 넣는다고 한다면

자동 형변환이 일어나게 된다.


2021-11-06 추가 업데이트

자바 공식문서 페이지를 보면서 추가적으로 여러개를 더 정리해보았다.

📌 변수의 종류

📌 인스턴스 변수 (비정적 필드)

static 키워드 없이 선언된 필드에 저장한다.

해당 값이 클래스의 각 인스턴스(각 개체마다)에 고유하기 때문에 인스턴스 변수라고 한다.

📌 클래스 변수(static 필드)

이 변수는 클래스가 인스턴스화 된 횟수에 상관없이 정확히 한개만 있음을 컴파일러에게 알려준다.

static은 초기화 후에 변경되지 않게 하려고 final을 같이 붙여서 사용해준다.

📌 지역 변수

객체가 필드에 상태를 저장하는 방식과 유사하게, 메서드는 임시 상태를 지역변수에 저장한다.

지역변수는 선언된 메서드에서만 볼 수 있다. 나머지 클래스에서는 액세스 불가능

📌 매개변수

흔히 보던 main메서드

public static void main(String[] args){} 에서 args가 바로 매개 변수이다.

매개변수는 항상 필드 가 아닌 변수 로 분류된다는 것이다.

728x90

'Java' 카테고리의 다른 글

변성  (0) 2022.08.11
일급 컬렉션  (0) 2022.08.09
상태 패턴 적용  (0) 2022.08.07
프로젝트 리팩토링  (0) 2022.08.07
728x90

스테이트 패턴 적용기

모든 코드는 깃허브에 있음을 알린다.

현재 패스트캠퍼스의 디자인 패턴 강의를 수강도 하고있고, 동시에 디자인 패턴에 대한 책도 한권 읽었었다.

그러니까 메인 메소드만 있는곳에서 디자인패턴을 적용하여 연습을 조금 했었다.

그리고 프로젝트에 도입을 했었는데 약간 2% 부족한 패턴이 탄생했었다.

지금에서의 완벽한 디자인패턴

프로젝트를 통해서 유저 포인트를 조건에 따라 변경해주는 로직이 있었다.

스크린샷 2021-10-06 오후 11 19 38

자... 예전의 나는 보잘 것 없었다.

ㅋㅋㅋㅋ 이게 그때 당시의 나에겐 최선이었을지도??

아무튼 각설하고 저 로직을 이제 바꿔보려고 한다.

해당로직의 bean이라는 객체는 단지 저 포인트와 해당 유저의 아이디만 가지고 있는 일종의 DTO이고,

네이밍마저 가독성을 해쳤다.

이제부터 리팩토링 과정을 적어보려고 한다.

리팩토링 과정

스크린샷 2021-10-06 오후 11 31 24

어설프지만 클래스 다이어그램을 그려보았다.

일단 PointState가 최종적으로 저 4개의 랭크들을 주입받아 사용하기 때문에 화살표를 그어주지는 않았다.

예전 방식으로는 저 클래스 하나하나마다 빈을 등록해주고 switch를 배제한대신 if문에 return방식으로 구현했다. 물론 뒷단으로 로직을 빼긴했지만 뒤지다보면 어찌됐든 나오게 되어있는 방식이었다...

일단 어떻게 구현했는지 하나하나 보도록하자

일단 기본 틀이되는 Rank부터 살펴보자

Rank.java

public interface Rank {
    int getRankEnum();
    int giveUserPoint();
}

랭크를 확인할 수 있는 로직과, point를 세팅해주는 로직이 있다.

RankEnum.java

@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum RankEnum {
    FIRST(1),
    SECOND(2),
    THIRD(3),
    ANOTHER(-1);

    private int rank;
}

랭크별 enum을 관리하기로 생각해서 따로 빼주었다.

public class First implements Rank {

    @Override
    public int getRankEnum() {
        return RankEnum.FIRST.getRank();
    }

    @Override
    public int giveUserPoint() {
        return 3000;
    }

}

나머지 클래스들은 안에 반환값만 다르기 때문에 First만 가져오겠다.

PointState.java

public class PointState {
    private final Map<Integer, Rank> pointStateMap = new HashMap<>();

    public PointState(List<Rank> ranks) {
        ranks.forEach(rank -> {
            pointStateMap.put(rank.getRankEnum(), rank);
        });
    }

    public Rank getPointStateMap(int rank) {
        return pointStateMap.get(rank);
    }

}

구현체인 PointState이다.

@Configuration
public class RankConfig {

    @Bean
    public PointState pointState() {
        return new PointState(Arrays.asList(new First(), new Second(), new Third(), new Another()));
    }

}

나는 이렇게 Config 클래스에서 런타임때에 초기화를 해주는데 생성자의 형식에 맞게
Rank에 해당하는 4가지 구현체를 넣어주었다.
이제 int형의 랭크 등급에 따라 추가할 포인트값을 받을 수 있게 되었다.

이제는 이게 정상적으로 동작하는지 확인을 해보기 위해 테스트코드를 먼저 짜도록 해보겠다.

테스트코드 작성

PointState.java

@SpringBootTest(classes = RankConfig.class)
class PointStateTest {

    @Autowired
    PointState pointState;

    @Test
    void test() {
        Rank rank = pointState.getPointStateMap(RankEnum.FIRST.getRank());

        assertThat(rank.getRankEnum()).isEqualTo(RankEnum.FIRST.getRank());
        assertThat(rank.giveUserPoint()).isEqualTo(3000);
    }

}

Rank설정을 해준 RankConfig를 가져온 후에 적용시켰다.

그렇지 않으면 마이바티스가 섞여있기 때문에 지금 에러가 발생한다 😱

스크린샷 2021-10-06 오후 11 44 16

정상적으로 통과하는것을 볼 수 있다.

이제 switch 로직을 리팩토링 할 생각이다.

스크린샷 2021-10-06 오후 11 45 29

이렇게 바꾸었고 이제 조건에 분기하여 포인트 값을 할당하지 않고 바로 작업할 수 있게 되었다!!!

정리

스크린샷 2021-10-06 오후 11 19 38

이랬던 로직이

스크린샷 2021-10-06 오후 11 45 29

이렇게 바뀐거 보고 디자인패턴 정말 좋다고 느낀다.

하나하나 배우면서 계속 적용해보고 싶어지는게 당연하달까..

아무튼 좋은 경험이었고 프로젝트 리팩토링이 잘되어가는것 같다 👍

728x90

'Java' 카테고리의 다른 글

일급 컬렉션  (0) 2022.08.09
변수  (0) 2022.08.07
프로젝트 리팩토링  (0) 2022.08.07
디자인 패턴 - Bridge Pattern  (0) 2022.08.07
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

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

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

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

이번 포스팅에서는 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

스터디도 시작한지 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

+ Recent posts