네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'web'에 해당되는 글 82건


반응형

2. 커버링 인덱스 사용하기

No Offset 페이징을 사용할 수 없는 상황이라면 커버링 인덱스로 성능을 개선할 수 있다.

커버링 인덱스란, 쿼리를 충족시키는데 필요한 모든 데이터를 갖고 있는 인덱스를 말한다.

즉, select, where, order by, limit, group by 등에서 사용되는 모든 컬럼이 Index 컬럼 안에 다 포함되는 경우다.

 

select 절까지 포함하게 되면 너무 많은 컬럼이 인덱스에 포함되기 때문에 실제론 커버링 인덱스는 select 를 제외한 나머지만 우선적으로 수행한다.

SELECT *
FROM items
WHERE [조건문]
ORDER BY id DESC
OFFSET [페이지 번호]
LIMIT [페이지 사이즈]

위와 같은 페이징 쿼리를 아래처럼 처리한 코드를 얘기한다.

SELECT *
FROM items as i 
JOIN (SELECT id
        FROM items
        WHERE [조건문]
        ORDER BY id DESC
        OFFSET [페이지 번호]
        LIMIT [페이지 사이즈]) as temp ON temp.id = i.id

위 쿼리에서 커버링 인덱스가 사용된 부분이 JOIN에 있는 쿼리이다.

SELECT id
FROM items
WHERE [조건문]
ORDER BY id DESC
OFFSET [페이지 번호]
LIMIT [페이지 사이즈]

select 절을 비롯해 order by, where 등 내 모든 항목이 인덱스 컬럼으로만 이루어지게 하여 인덱스 내부에서 쿼리가 완성될 수 있도록 하는 방식이다.

이렇게 커버링 인덱스로 빠르게 걸러낸 id를 통해 실제 select 절의 항목들을 빠르게 조회하는 방식이다.

2-1. 커버링 인덱스 사용하기

일반적으로 인덱스를 이용해 조회되는 쿼리에서 가장 큰 성능 저하를 일으키는 부분은 인덱스를 검색하고 대상이 되는 row의 나머지 컬럼 값을 데이터 블록에서 읽을 때이다.

- 페이징 쿼리와 무관하게 인덱스를 추가해도 느린 쿼리는 select절 때문이다.

커버링 인덱스를 태우지 않은 일반 조회 쿼리는 order by, offset ~ limit 을 수행할 때도 데이터 블록으로 접근을 하게 된다.

반대로 커버링 인덱스 방식을 사용하면 where, order by, offset ~ limit 을 인덱스 검색으로 빠르게 처리하고, 이미 다 걸러진 몇 개의 row에 대해서만 데이터 블록에 접근하기 때문에 성능의 이점을 얻게 된다.

 

앞서 1. No Offset을 사용한 페이징 성능 개선에서 사용된 페이징 쿼리는 아래와 같다.

SELECT id, book_no, book_type, name
FROM book
WEHRE name LIKE 'a%'
ORDER BY id DESC
LIMIT 10 OFFSET 10000;

SELECT에서 사용된 book_no, book_type은 인덱스 (idx_book_1(name))에 포함하지 않기 때문에 커버링 인덱스가 될 수 없다.

2-2. 구현 코드

 Querydsl 

Querydsl에서 커버링 인덱스를 사용해야 한다면 2개의 쿼리로 분리해서 진행할 수 밖에 없다.

(이유는 Querydsl에서 from 절의 서브 쿼리를 지원하지 않기 때문이다.)

- 커버링 인덱스를 활용해 조회 대상의 PK를 조회

- 해당 PK로 필요한 컬럼 항목들 조회

pubblic List<BookPaginationDto> paginationCoveringIndex(String name, int pageNo, int pageSize) {

    // 1. 커버링 인덱스로 대상 조회 (id만 포함하여 커버링 인덱스를 활용해 빠르게 조회)
    List<Long> ids = queryFactory
                        .select(book.id)
                        .from(book)
                        .where(book.name.like(name + "%"))
                        .orderBy(book.id.desc())
                        .limit(pageSize)
                        .offset(pageNo * pageSize)
                        .fetch();
    
    // 1-1. 대상이 없을 경우 추가 쿼리 수행할 필요 없이 바로 변환
    if (CollectionUtils.isEmpty(ids)) {
        return new ArrayList<>();
    }
    
    // 2. 
    return queryFactory
             .select(Projections.fields(BookPaginationDto.class,
                     book.id.as("bookId"),
                     book.name,
                     book.bookNo,
                     book.bookType))
             .from(book)
             .where(book.id.in(ids))
             .orderBy(book.id.desc()) // where in id 만 있어 결과 정렬이 보장되지 않는다.
             .fetch();
}

JdbcTemplate

문자열 쿼리를 직접 사용하는 방식인데, 이렇게 진행하면 querydsl를 쓸 때처럼 쿼리를 분리할 필요 없이, 커버링 인덱스를 from 절에 그대로 사용하면 된다.

public List<BookPaginationDto> paginationCoveringIndexSql(String name, int pageNo, int pageSize) {
String query =
        "SELECT i.id, book_no, book_type, name " +
        "FROM book as i " +
        "JOIN (SELECT id " +
        "       FROM book " +
        "       WHERE name LIKE '?%' " +
        "       ORDER BY id DESC " +
        "       LIMIT ? " +
        "       OFFSET ?) as temp on temp.id = i.id";

return jdbcTemplate
        .query(query, new BeanPropertyRowMapper<>(BookPaginationDto.class),
                name,
                pageSize,
                pageNo * pageSize);
}

2-3. 단점

커버링 인덱스 방식은 일반적인 페이징 방식에서는 거의 대부분 적용할 수 있는 효과적인 개선 방법이지만 단점이 존재한다.

 

- 너무 많은 인덱스가 필요하다.

결국 쿼리의 모든 항목이 인덱스에 포함되어야 하기 때문에 느린 쿼리가 발생할 때마다 인덱스가 신규 생성될 수도 있다.

- 인덱스 크기가 너무 커진다.

인덱스도 결국 데이터기 때문에 너무 많은 항목이 들어가면 성능 이슈가 발생할 수 있다. 

- 데이터 양이 많아지고, 페이지 번호가 뒤로 갈 수록 No Offset에 비해 느리다.

 

 

 

 출처 

https://jojoldu.tistory.com/529 

 

2. 페이징 성능 개선하기 - 커버링 인덱스 사용하기

2. 커버링 인덱스 사용하기 앞서 1번글 처럼 No Offset 방식으로 개선할 수 있다면 정말 좋겠지만, NoOffset 페이징을 사용할 수 없는 상황이라면 커버링 인덱스로 성능을 개선할 수 있습니다. 커버링

jojoldu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

 

페이징은 웹에서 흔히 볼 수 있는 기능이다. 하지만, 기초적인 페이징 구현 방식은 서비스가 커지면서 큰 장애를 유발할 수 있다. 적재된 데이터가 많아지면서 페이징 기능이 수십초~ 수분까지 조회가 느려질 수 있다.

 

특히 1억건이 넘는 테이블에서의 페이징은 단순히 인덱스를 추가한다고 해서 성능 문제가 해결되진 않는다.

 

그래서 일반적인 페이징 기능에서 성능을 개선하는 방법을 알아보자.

물론, 인덱스를 이용한 쿼리 튜닝이 되어있다는 가정이고 조회 쿼리에서 인덱스 사용은 필수이다.

 

 [ 참고 ] 효율적인 데이터베이스 인덱스 설정 방법 

https://frogand.tistory.com/144

 

[SQL] 효율적인 DB INDEX(인덱스) 설정

인덱스(INDEX) 검색 속도를 높이기 위한 색인 기술이다. 보통 인덱스는 일반적으로 SELECT 쿼리의 WHERE에 사용할 컬럼에 대해 효율적인 검색을 위해 사용하거나, 다른 테이블과의 JOIN에 사용된다. (

frogand.tistory.com

 

1. No Offset으로 구조 변경하기

기존 페이징에서 No Offset으로 구조를 변경하는 것이다.

기존 페이징 방식은 페이지 번호(offset)와 페이지 사이즈(limit)를 기반으로 한다.

반면에 No Offset은 아래와 같이 페이지 번호(offset)이 없는 더보기(More) 방식을 이야기한다.

 

 

1-1. No Offset은 왜 빠른가?

기존에 사용하는 페이징 쿼리는 일반적으로 다음과 같다.

SELECT * 
FROM items
WHERE [조건]
ORDER BY id DESC
OFFSET [페이지번호]
LIMIT [페이지사이즈]

이와같은 형태의 페이징 쿼리가 뒤로 갈수록 느린 이유는 결국 앞에서 읽었던 행을 다시 읽어야 하기 때문입니다.

예를 들어 offset 10,000이고 limit 20이라 하면 최종적으로 10,020개의 행을 읽어야 합니다.

그리고 이 중의 얖 10,000개 행은 버리게 됩니다.

뒤로 갈 수록 버리지만 읽어야 할 행의 개수가 많아 점점 뒤로 갈수록 느려집니다.

 

 

No Offset 방식은 바로 이 부분에서 조회 시작 부분을 인덱스로 빠르게 찾아 매번 첫 페이지만 읽도록 하는 방식입니다.

(클러스터 인덱스인 PK를 조회 시작 부분 조건문으로 사용했기 때문에 빠르게 조회됩니다.)

SELECT *
FROM items
WHERE [조건문] 
AND id < [마지막 조회 ID] # 직전 조회 결과의 마지막 id
ORDER BY id DESC
LIMIT [페이지 사이즈]

이전에 조회된 결과를 한번에 건너뛸 수 있게 마지막 조회 결과의 ID를 조건문에 사용하는 것으로 매번 이전 페이지 전체를 건너 뛴다.

즉, 아무리 페이지가 뒤로 가더라도 처음 페이지를 읽은 것과 동일한 성능을 가지게 된다.

 

1-2. 구현 코드

기존의 페이징 코드 (사용되는 book 엔티티의 인덱스는 idx_boox_1 (name) 이다.)

public List<BookPaginationDto> paginationLegacy(String name, int pageNo, int pageSize) {

    return queryFactory
            .select(Projections.fields(BookPaginationDto.class,
                    book.id.as("bookId"),
                    book.name,
                    book.bookNo))
            .from(book)
            .where(book.name.like(name + "%")) // like 뒤에는 %가 있을 때만 인덱스가 적용된다.
            .orderBy(book.id.desc()) // 최신순으로
            .limit(pageSize) // 지정된 사이즈만큼
            .offset(pageNo * pageSize) // 지정된 페이지 위치에서
            .fetch(); // 조회
}

offset은 pageNo가 사용되는 것이 아니라 몇번째 row부터 시작할지를 나타낸다.

pageNo는 0부터 시작한다고 가정한 상태이다. 1부터 시작할 경우 (pageNo - 1) * pageSize를 사용해야 한다.

 

No Offset 코드 1

public List<BookPaginationDto> paginationNoOffsetBuilder(Long bookId, String name, int pageSize) {

    BooleanBuilder dynamicLtId = new BooleanBuilder();
    
    if (bookId != null) {
        dynamicLtId.and(book.id.lt(bookId));
    }
    
    return queryFactory
        .select(Projections.fields(BookPaginationDto.class,
                book.id.as("bookId"),
                book.name,
                book.bookNo))
        .from(book)
        .where(dynamicLtId
                .and(book.name.like(name + "%")))
        .orderBy(book.id.desc())
        .limit(pageSize)
        .fetch();
}

첫 페이지 조회할 때와 두번째 페이지부터 조회할 때 사용되는 쿼리가 달라 동적 쿼리가 필요하다.

그래서 맨 위와 같은 코드가 사용되었다. (BooleanBuilder 사용) 

 

No Offset 코드 2

public List<BookPaginationDto> paginationNoOffsetBuilder(Long bookId, String name, int pageSize) {
    return queryFactory
        .select(Projections.fields(BookPaginationDto.class,
                book.id.as("bookId"),
                book.name,
                book.bookNo))
        .from(book)
        .where(ltBookId(bookId), book.name.like(name + "%"))
        .orderBy(book.id.desc())
        .limit(pageSize)
        .fetch();
}

private BooleanExpression ltBookId(Long bookId) {
    if (bookId == null) {
        return null; // BooleanExpression 자리에 null이 반환되면 조건문에서 자동으로 삭제된다.
    }
    return book.id.lt(bookId);
}

 

1-3. 테스트 코드로 기능 검증

기존 페이징 기능의 테스트이다.

@Test
void 기존_페이징_방식() throws Exception {
    // given
    String prefixName = "a";
    for (int i = 0; i <= 30; i++) {
        Book book = Book.builder()
                    .name(prefixName + i) 
                    .bookNo(i)
                    .build();
        bookRepository.save(book);
    }
    
    // when
    // 두번째 페이지 조회 (pageNo는 0부터 시작임)
    List<BookPaginationDto> books = bookPaginationRepository.paginationLegacy(prefix, 1, 10);
    
    // then
    assertThat(books).hasSize(10);
    assertThat(books.get(0).getName()).isEqualsTo("a20");
    assertThat(books.get(9).getName()).isEqualsTo("a11");
}

 

NoOffset 테스트

@Test
void nooffset_첫페이지() throws Exception {
    // given
    String prefixName = "a";
    for (int i = 0; i <= 30; i++) {
        Book book = Book.builder()
                    .name(prefixName + i) 
                    .bookNo(i)
                    .build();
        bookRepository.save(book);
    }
    
    // when
    List<BookPaginationDto> books = bookPaginationRepository.paginationNoOffset(null, prefixName, 10);
    
    // then
    assertThat(books).hasSize(10);
    assertThat(books.get(0).getName()).isEqualsTo("a30");
    assertThat(books.get(9).getName()).isEqualsTo("a21");
}

@Test
void nooffset_두번째페이지() throws Exception {
    // given
    String prefixName = "a";
    for (int i = 0; i <= 30; i++) {
        Book book = Book.builder()
                    .name(prefixName + i) 
                    .bookNo(i)
                    .build();
        bookRepository.save(book);
    }
    
    // when
    List<BookPaginationDto> books = bookPaginationRepository.paginationNoOffset(21L, prefixName, 10);
    
    // then
    assertThat(books).hasSize(10);
    assertThat(books.get(0).getName()).isEqualsTo("a20");
    assertThat(books.get(9).getName()).isEqualsTo("a11");
}

 

1-4. 성능 비교

offset 위치 / 인덱스 유무 / DB 성능에 따라 테스트 결과가 다를 수 있지만 1억 row, 5개의 컬럼의 경우 수백배의 성능 차이가 난다.

 

1-5. 단점

- where에 사용되는 기준 Key가 중복이 가능할 경우

group by 등으로 기준을 잡을 key가 중복이 될 경우 정확한 결과를 반환할 수 없다.

- More 버튼이 아니라 페이징 버튼 형식으로 해야할 경우

 

 출처 

https://jojoldu.tistory.com/528

 

1. 페이징 성능 개선하기 - No Offset 사용하기

일반적인 웹 서비스에서 페이징은 아주 흔하게 사용되는 기능입니다. 그래서 웹 백엔드 개발자분들은 기본적인 구현 방법을 다들 필수로 익히시는데요. 다만, 그렇게 기초적인 페이징 구현 방

jojoldu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

[Spring] Assert


반응형

Assert는 단순히 if문을 줄이는 역할만 하는 것이 아니다. 프로젝트 규칙을 적용하고 공통을 재사용한다는 것에 큰 의미가 있다. 

 

Spring Assert를 사용하는 목적

Spring Assert는 인수를 검증하고 조건에 맞지 않는 경우 IllegalArgumentException 또는 IllegalStateException를 발생시킨다. 

이 부분은 조건문을 단순화하고 반복적인 코드를 줄이는 역할을 한다.

if (user == null) {
    throw new IllegalArgumentException("사용자 정보가 존재하지 않습니다.");
}
// 위 코드는 아래와 같이 바꿀 수 있다.
Assert.notEmpty(user, "사용자 정보가 존재하지 않습니다.");

 

Assert의 확장

값을 검증하는 방법을 여러 형태로 만들어 낼 수 있다. 또한 exception도 다양하게 사용할 수 있다. 그런데 프로젝트를 하다보면 값을 검증하는 방법을 통합하거나 예외의 사용을 제한할 필요가 있다. 

 

public class JiyoonAssert extends Assert {
 
    private static final String NUMBER_ALPHABET_PATTERN = "^[a-zA-Z\\d]+$";
    private static final String NUMERIC_PATTERN = "^[\\+\\-]{0,1}[\\d]+$";
    private static final String FLOAT_PATTERN = "^[\\+\\-]{0,1}[\\d]+[\\.][0-9]+$";
 
    /**
     * Assert a boolean expression, TRUE가 아닌 경우 사용자가 정의한 예외를 던진다.
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.state(id == null, "The id property must not already be initialized", EbloInvalidRequestException.class);</pre>
     * @param expression a boolean expression
     * @param message the exception message to use if the assertion fails
     * @param exceptionClass
     * @throws IllegalStateException if {@code expression} is {@code false}
     */
    public static void state(boolean expression, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (!expression) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * Assert a boolean expression, TRUE가 아닌 경우 사용자가 정의한 예외를 던진다.
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.isTrue(user != null, "사용자 정보가 존재하지 않습니다.", EbloNotFoundException.class);</pre>
     * @param expression a boolean expression
     * @param message the exception message to use if the assertion fails
     * @param exceptionClass
     * @throws IllegalStateException if {@code expression} is {@code false}
     */
    public static void isTrue(boolean expression, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (!expression) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * Assert that an object is {@code null}. 객체가 null이 아닌 경우 사용자가 정의한 예외를 던진다.
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.isNull(user, "기존 사용자 정보가 존재합니다.", EbloInvalidRequestException.class);</pre>
     * @param expression a boolean expression
     * @param message the exception message to use if the assertion fails
     * @param exceptionClass
     * @throws IllegalStateException if {@code expression} is {@code false}
     */
    public static void isNull(@Nullable Object object, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (object != null) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * null인 경우 사용자 정의 예외 발생
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.notNull(user, "사용자 정보가 존재하지 않습니다.", EbloNotFoundException.class);</pre>
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void notNull(@Nullable Object object, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (object == null) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * 전달받은 값이 null이 거나 빈값인 경우 사용자 정의 예외 발생
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.hasLength(value, "전달 받은 값이 빈값입니다.", EbloInvalidRequestException.class);</pre>
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void hasLength(@Nullable String text, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (!StringUtils.hasLength(text)) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * 전달받은 값이 null이 거나 빈값인 경우 사용자 정의 예외 발생
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.hasText(value, "전달 받은 값이 빈값입니다.", EbloInvalidRequestException.class);</pre>
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void hasText(@Nullable String text, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (!StringUtils.hasText(text)) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * 전달받은 값이 null이 거나 빈값인 경우 사용자 정의 예외 발생
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.notEmpty(array, "전달 받은 값이 빈값입니다.", EbloInvalidRequestException.class);</pre>
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void notEmpty(@Nullable Object[] array, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (ObjectUtils.isEmpty(array)) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * 전달받은 값이 null이 거나 빈값인 경우 사용자 정의 예외 발생
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.notEmpty(collection, "전달 받은 값이 빈값입니다.", EbloInvalidRequestException.class);</pre>
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void notEmpty(@Nullable Collection<?> collection, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (CollectionUtils.isEmpty(collection)) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * 전달받은 값이 null이 거나 빈값인 경우 사용자 정의 예외 발생
     * 사용자 정의 예외는 AbstractEbloBaseException.class를 상속 받아 구현해야 한다. 
     * <pre class="code">Assert.notEmpty(map, "전달 받은 값이 빈값입니다.", EbloInvalidRequestException.class);</pre>
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void notEmpty(@Nullable Map<?, ?> map, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if (CollectionUtils.isEmpty(map)) {
            throwException(message, exceptionClass);
        }
    }
 
    private static void throwException(String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        try {
            throw exceptionClass.getDeclaredConstructor( String.class ).newInstance( message );
        } catch (Exception e) {
            e.printStackTrace();
            throw new EbloSystemException("예외 처리 중 오류가 발생했습니다. "+e.getMessage());
        }
    }
    
    /**
     * 값이 영문 알파벳, 숫자가 아닌 경우 예외 발생 
     * @param object
     * @param message
     */
    public static void isAlphaNumber(String object, String message) {
        isMatched(object, NUMBER_ALPHABET_PATTERN, message, EbloInvalidRequestException.class);
    }
 
    /**
     * 값이 영문 알파벳, 숫자가 아닌 경우 사용자 정의 예외 발생 
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void isAlphaNumber(String object, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        isMatched(object, NUMBER_ALPHABET_PATTERN, message, exceptionClass);
    }
    
    /**
     * 값이 숫자가 아닌 경우 예외 발생 
     * @param object
     * @param message
     */
    public static void isNumeric(String object, String message) {
        isMatched(object, NUMERIC_PATTERN, message, EbloInvalidRequestException.class);
    }
    
    /**
     * 값이 숫자가 아닌 경우 사용자 정의 예외 발생 
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void isNumeric(String object, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        isMatched(object, NUMERIC_PATTERN, message, exceptionClass);
    }
    
    /**
     * 값이 float, double이 아닌 경우 예외 발생 
     * @param object
     * @param message
     */
    public static void isFloat(String object, String message) {
        isMatched(object, FLOAT_PATTERN, message, EbloInvalidRequestException.class);
    }
    
    /**
     * 값이 float, double이 아닌 경우 사용자 정의 예외 발생 
     * @param object
     * @param message
     * @param exceptionClass
     */
    public static void isFloat(String object, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        isMatched(object, FLOAT_PATTERN, message, exceptionClass);
    }
    
    /**
     * 패턴 매치, 해당 패턴과 일치 하지 않는 경우 사용자 정의 예외 발생 
     * @param object
     * @param pattern
     * @param message
     * @param exceptionClass
     */
    public static void isMatched(String object, String pattern, String message, final Class<? extends AbstractEbloBaseException> exceptionClass) {
        if(object == null || "".equalsIgnoreCase(object)) return;
        
        if(!object.matches(pattern)) {
            throwException(message, exceptionClass);
        }
    }
 
    /**
     * 패턴 매치, 해당 패턴과 일치 하지 않는 경우 예외 발생 
     * @param object
     * @param pattern
     * @param message
     */
    public static void isMatched(String object, String pattern, String message) {
        if(object == null || "".equalsIgnoreCase(object)) return;
        
        if(!object.matches(pattern)) {
            throwException(message, EbloInvalidRequestException.class);
        }
    }
    
}
@Test
public void test() {
    JiyoonAssert.isTrue( 1 > 0, "test");
}

 

 

 

 

 출처 

https://eblo.tistory.com/63

 

Spring boot - Assert 사용 예제

1. Overview Assert는 단순히 if문을 줄이는 역할만 하는 것은 아닙니다. 프로젝트 규칙을 적용하고 공통을 재사용한다는 것에 큰 의미가 있습니다. 복잡한 기술이 필요한 것도 아니고 누구나 쉽게

eblo.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

redirect를 사용하여 파라미터를 넘겨줘야하는 경우가 있다.

RedirectAttributes 인터페이스를 통해 전달해주는데

파라미터를 담어서 넘겨줄 때 사용하는 함수는 총 3개로 

- addAttribute

- addAllAttributes

- addFlashAttribute

이다.

 

addAttribute

흔히 사용하는 addAttribute는 해당 페이지로 리다이렉트할 때 값을 넘겨주는 용도로 사용한다.

addAttribute를 사용하면 URL 뒤에 붙어 값이 유지가 된다.

 

addFlashAttribute

하지만 addFlashAttribute로 전달한 값은 URL에 존재하지 않는다. 일회성으로 URL에 붙지 않고 세션 후 재지정 요청이 들어오면 값이 사라지는 휘발성 성질을 가지고 있다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

@AuthenticationPrincipal

 

@GetMapping("/")
public String index(Model model, Principal principal) {

    if (principal == null) {
        model.addAttribute("message", "Spring security");
    } else {
        model.addAttribute("message", "Hello, " + principal.getName());
    }
    
    return "index";
}

로그인한 사용자의 정보를 파라미터로 받아오고 싶을 때, Java 표준 Principal 객체를 받아서 사용한다. Java 표준 Principal 객체는 name 정보만 참조할 수 있다.

 

@AuthenticationPrincipal 어노테이션을 사용하면 UserDetailsService에서 Return한 객체를 파라미터로 직접 받아 사용할 수 있다.

@GetMapping("/")
public String index(Model model, @AuthenticationPrincipal User user) {

    if (user == null) {
        model.addAttribute("message", "Spring security");
    } else {
        model.addAttribute("message", "Hello, " + user.getName());
    }
    
    return "index";
}

현재 로그인한 사용자의 정보를 참조하고 싶을 때 도메인의 User를 나타내는 객체 (Account)를 직접 사용하고 싶다는 요구사항이 있다면?

 

Adapter 클래스

- UserDetailsService에서 리턴하는 타입을 변경하면, Controller에서 @AuthenticationPrincipal로 받아올 수 있는 객체가 변경된다.

- 이때 사용할 수 있는 방법은 두가지이다.

1. Account 객체를 직접 리턴하기

2. Account 객체를 감싸는 Adapter 클래스를 사용하기

 

Account 객체를 직접 리턴하는 방법은 Account 객체가 UserDetails를 구현해야 한다.

도메인 객체는 특정 기술에 종속되지 않도록 개발하는 것이 좋다.

 

AccountAdapter.java

@Getter
public class AccountAdapter extends User {

    private Account account;

    public AccountAdapter(Account account) {
        super(account.getUserId(), account.getPassword(), authorities(account.getRoles()));
        this.account = account;
    }

    private static Collection<? extends GrantedAuthority> authorities(Set<AccountRole> roles) {
        return roles.stream()
                .map(r -> new SimpleGrantedAuthority(r.getRole().authority()))
                .collect(Collectors.toSet());
    }
}

- User 클래스를 상속받는다.

- AccountAdapter의 멤버는 오로지 Account 객체만 존재한다.

- 생성자 내부에서 User 클래스의 생성자를 호출하여 username, password, role을 세팅한다.

 

User 클래스를 상속받는 이유는?

- UserDetailsService에서 리턴하는 객체는 UserDetails 타입이어야 한다.

- UserDetails를 구현하는 User 클래스를 상속받는 방식으로 사용한다.

 

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUserId(username);
        if (account == null) {
            throw new UsernameNotFoundException("user not found");
        }

        return new AccountAdapter(account);
    }

 

AccountAdapter 사용하기

@GetMapping("/")
public String index(Model model, @AuthenticationPrincipal AccountAdapter accountAdapter) {

    if (accountAdapter == null) {
        model.addAttribute("message", "Spring security");
    } else {
        model.addAttribute("message", "Hello, " + accountAdapter.getAccount().getUserId());
    }
    
    return "index";
}

accountAdapter.getAccount()로 Account 객체를 참조하지 말고 Account 객체를 직접 사용하도록 하자.

 

Account 객체 직접 사용하기

@AuthenticationPrincipal은 SpEL을 지원한다.

SpEL을 사용해서 Adapter 클래스가 아닌 Account 객체를 직접 가져올 수 있다.

@GetMapping("/")
public String index(Model model, @AuthenticationPrincipal(expression = "#this == 'anonymousUser ? null : account") Account account) {

    if (account == null) {
        model.addAttribute("message", "Spring security");
    } else {
        model.addAttribute("message", "Hello, " + account.getUserId());
    }
    
    return "index";
}

만약 현재 참조중인 객체가 AnonymousAuthenticaionFilter에 의해 생성된 Authentication인 경우 null을 반환하고, 아니라면 AccountAdapter 객체로 간주하고 Account 객체를 반환한다.

 

@CurrentUser

SpEL을 사용해서 직접 Account 객체를 가져오긴 했지만 해당 코드가 너무 길다.

- 커스텀 어노테이션을 생성하여 해결하자

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentUser {
}
@GetMapping("/")
public String index(Model model, @CurrentUser Account account) {

    if (account == null) {
        model.addAttribute("message", "Spring security");
    } else {
        model.addAttribute("message", "Hello, " + account.getUserId());
    }
    
    return "index";
}

 

 

 정리 

- @AuthenticationPrincipal을 사용하여 UserDetailsService에서 리턴한 객체를 컨트롤러의 파라미터로 직접 참조할 수 있다.

- 만약 도메인의 User를 표현하는 클래스(Account)를 직접 참조하고 싶다면 Adapter 클래스를 사용하자

 

 출처 

https://ncucu.me/137

 

Spring Security - @AuthenticationPrincipal

Spring Security - @AuthenticationPrincipal @AuthenticationPrincipal 로그인한 사용자의 정보를 파라메터로 받고 싶을때 기존에는 다음과 같이 Principal 객체로 받아서 사용한다. 하지만 이 객체는 SecurityCo..

ncucu.me

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !