(101)

Spring Security WebSecurityConfigurerAdapter / cors, csrf Deprecated 해결하기

서론최근 토이 프로젝트 진행중에 간단하게 회원기능을 구현하고자 Security를 사용하는데 이전 사용했던 WebSecurityConfigurerAdapter을 상속받아 구현하던 Config설정에 바뀐 부분이 있고 또한 시큐리티를 통한 권한 확인이나 로그인, 로그아웃 등 기타 작업등은 하지 않을 계획이라 csrf(), cors() disable을 설정하는 과정에서도 Deprecated된 것을 확인하여 기록하고자 함 아래는 각각 WebSecurityConfigurerAdapter와, Spring Security 6.1.2버전에서의 Deprecated API를 정리해둔 공식 docs https://spring.io/blog/2022/02/21/spring-security-without-the-websecuri..

QueryDSL 사용하기(SpringBoot 3.0) - QueryDSL 사용하기(4)

서론 드디어 아래의 쿼리를 QueryDSL로 뽑아내서 View시켜볼 수 있게 되었다. select C.CHAMPION_ID, C.PRICE, ROUND((C.PRICE-CP.PRICE)/CP.PRICE*100) as PERCENT from Champion C, (select CC.CHAMPION_ID, CC.PRICE from ChampionPriceLog CC where (CC.CHAMPION_ID, CC.CREATE_DATE) in(select CCC.CHAMPION_ID, MAX(CCC.CREATE_DATE) AS CREATE_DATE from ChampionPriceLog CCC group by CCC.CHAMPION_ID) order by CC.CHAMPION_ID) as CP where C.C..

QueryDSL JPA Test (Spring Boot 3.0) - QueryDSL 사용하기(3)

셋팅 def queryDslVersion = '5.0.0' dependencies { //QueryDsl // 필수 implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta" implementation "com.querydsl:querydsl-core:${queryDslVersion}" // QueryDsl 쿼리 타입 생성 (QClass 생성 시 @Entity 탐색) annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta" // java.lang.NoClassDefFoundError:javax/persistence/Entity 에러 방지 annotationProc..

QueryDSL이란? - QueryDSL 사용하기(1)

서론 토이 프로젝트 진행 중 아래와 같은 쿼리 사용이 필요했고, QueryDSL을 한 번 사용해보기로 했음. select C.CHAMPION_ID, C.PRICE, ROUND((C.PRICE-CP.PRICE)/CP.PRICE*100) as PERCENT from Champion C, (select CC.CHAMPION_ID, CC.PRICE from ChampionPriceLog CC where (CC.CHAMPION_ID, CC.CREATE_DATE) in(select CCC.CHAMPION_ID, MAX(CCC.CREATE_DATE) AS CREATE_DATE from ChampionPriceLog CCC group by CCC.CHAMPION_ID) order by CC.CHAMPION_ID) as..

JPQL이란? - QueryDSL 사용하기(2)

서론 이전 글에 나왔던 키워드 중 JPQL에 대해 조금이라도 알아보기 위해 정리하는 글 JPQL(Java Persitence Query Language)이란? 엔티티 객체를 대상으로 하는 객체지향 쿼리로 SQL을 추상화한 객체지향 쿼리이며, 작성된 JPQL은 SQL로 변환된다. 기존 JPA의 메서드 호출만으로는 섬세한 쿼리 작성이 어렵다는 문제를 해결하기 위해 JPQL이 나타나게 되었으며 SQL을 추상화했기 때문에 특정 데이터베이스 SQL에 의존하지 않는다는 장점이 있다. SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN을 지원한다. 예시 Member 객체를 대상으로 이름에 "bazzi"가 포함된 모든 회원을 검색하는 쿼리 String jpql = "select m From ..

[Java] BufferedReader BufferedWriter

서론 요새 프로그래머스가 잠잠해져서 LeetCode 외에도 HackerRank라는 플랫폼에서도 코테문제를 풀고있는데, 얼른 플랫폼에 적응해서 적당한 난이도의 문제를 풀고 포스팅 하고 싶다 HackerRank - Online Coding Tests and Technical Interviews HackerRank is the market-leading technical assessment and remote interview solution for hiring developers. Start hiring at the pace of innovation! www.hackerrank.com 서론이 이상하게 흘러갔는데 여튼, 백준처럼 직접 입력받아 출력해야하는 포맷의 HackerRank인 만큼, 입출력에 대한 공부..

SynchronizedList / CopyOnWriteArrayList

[Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor)연관 게시물 https://mag1c.tistory.com/364 스레드 안전 - Thread Safe 연관 게시물 https://mag1c.tistory.com/365 [Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor) 자바에서의 Thread-Safe 1. Lock synchronized 아래 mag1c.tistory.com 스레드 안전 - Thread Safe연관 게시물 https://mag1c.tistory.com/365 [Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor) 자바에서의 Thread-Safe 1. Lock synchronized 아래 코드는 ..

ConcurrentHashMap이란? HashMap vs HashTable vs ConcurrentHashMap

Map의 인터페이스의 구현체로는 HashMap, HashTable, ConcurrentHashMap등이 있는데 thread-safe와 hashtable관련 포스팅을 했기 때문에 어떤 경우에 어떤 자료구조를 사용해야 하는지 알아보려 한다. [자료구조] 해시테이블 (hashtable)해시테이블 (hashtable) Key, Value 로 데이터를 저장하는 자료구조 중 하나이며 데이터를 빠르게 검색할 수 있는 자료구조이다. 빠른 검색을 할 수 있는 이유는 내부적으로 버킷(배열)을 사용하여 데mag1c.tistory.com [Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor)연관 게시물 https://mag1c.tistory.com/364 스레드 안전 - Thread Safe 연관..

[Project 회고록] 친구 추가 관련 기능 구현 시 테이블에 대한 고찰(친구 목록과 친구 요청에 대한 테이블을 나눌 것인가?)

개요 위 사진은 현재 개발중인 토이 프로젝트의 User - Friend의 테이블 구조이다. 친구 요청을 수락하여 서로 친구인 사용자 간의 개인 채팅방을 이용 가능하게 할 생각이었으며 친구 요청과 응답에 대한 프로세스를 하나의 테이블로 관리하고, 서로 친구가 된 친구목록을 또다른 테이블에 관리하려고 하였다. (여기서 말하는 친구 요청에 대한 응답은, 웹소켓으로 구현했음) 현재 요청과 응답에 대한 프로세스 또한 요청과 응답을 관리하는 테이블의 STATUS 컬럼의 도메인값을 NOREAD, READ, REJECT, ACCEPT 의 네 경우를 두고, 요청을 보낸 사용자가 친구 요청을 받은 사용자가 친구 요청을 보지 않았는지, 봤는데 수락도 거절도 아닌지, 거절했는지에 대한 상황을 View할 수 있게 설계했다. 수..

Spring Security WebSecurityConfigurerAdapter / cors, csrf Deprecated 해결하기

Tech/Java & Spring 2023. 8. 20. 13:06
728x90
728x90

서론

최근 토이 프로젝트 진행중에 간단하게 회원기능을 구현하고자 Security를 사용하는데
이전 사용했던 WebSecurityConfigurerAdapter을 상속받아 구현하던 Config설정에 바뀐 부분이 있고
또한 시큐리티를 통한 권한 확인이나 로그인, 로그아웃 등 기타 작업등은 하지 않을 계획이라
csrf(), cors() disable을 설정하는 과정에서도 Deprecated된 것을 확인하여 기록하고자 함
 
아래는 각각 WebSecurityConfigurerAdapter와, Spring Security 6.1.2버전에서의
Deprecated API를 정리해둔 공식 docs
 
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

Spring Security without the WebSecurityConfigurerAdapter

In Spring Security 5.7.0-M2 we deprecated the WebSecurityConfigurerAdapter, as we encourage users to move towards a component-based security configuration. To assist with the transition to this new style of configuration, we have compiled a list of common

spring.io

 
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html#csrf() 

HttpSecurity (spring-security-docs 6.1.2 API)

securityContext Deprecated, for removal: This API element is subject to removal in a future version. Returns: the SecurityContextConfigurer for further customizations Throws: Exception

docs.spring.io

 
 

본론

구글링을 통한 blog나 stackoverflow도 좋지만
아래처럼 공식문서를 통해 간단하게 해결할 수 있는 부분이다.

 
 
딱히 무언가 권한이나 기타 설정들은 하지 않을 것이기 때문에 아래처럼 Config파일을 구성할 수 있었다.
 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        return http
                .csrf((csrf) -> csrf.disable())
                .cors((cors) -> cors.disable())
                .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

QueryDSL 사용하기(SpringBoot 3.0) - QueryDSL 사용하기(4)

Tech/Java & Spring 2023. 8. 4. 05:34
728x90
728x90

서론

드디어 아래의 쿼리를 QueryDSL로 뽑아내서 View시켜볼 수 있게 되었다.

select C.CHAMPION_ID, C.PRICE, ROUND((C.PRICE-CP.PRICE)/CP.PRICE*100) as PERCENT
  from Champion C, (select CC.CHAMPION_ID, CC.PRICE
					  from ChampionPriceLog CC
					 where (CC.CHAMPION_ID, CC.CREATE_DATE) in(select CCC.CHAMPION_ID, MAX(CCC.CREATE_DATE) AS CREATE_DATE
																 from ChampionPriceLog CCC
																group by CCC.CHAMPION_ID)
					order by CC.CHAMPION_ID) as CP
 where C.CHAMPION_ID = CP.CHAMPION_ID
 order by C.CHAMPION_ID;

 

 

 

 

테스트

포스팅을 하게 만들었던 위의 쿼리를 사용하기 위해 QueryDSL을 사용해보기로 했다.

QChampion C = QChampion.champion;
QChampionPriceLog CPL = QChampionPriceLog.championPriceLog;

우선 필요한 QClass의 인스턴스를 생성한 뒤

 

List<Tuple> tupleList =
        (List<Tuple>) jpaQueryFactory.select(C.name, CPL.price, C.price)
                .from(CPL)
                .join(C)
                .on(CPL.champion_id.eq(C.id))
                .where(
                        Expressions.list(CPL.champion_id, CPL.create_date)
                                .in(
                                        jpaQueryFactory.select(
                                                        CPL.champion_id, CPL.create_date.max().as("create_date")
                                                )
                                                .from(CPL)
                                                .groupBy(CPL.champion_id)
                                )

                )
                .orderBy(CPL.champion_id.asc())
                .fetch();

조인을 실행한 결과를 Tuple형태로 반환시켰다.

Tuple은 QueryDSL에서 제공하는 컨테이너 클래스로, 쿼리 결과의 각 튜플을 나타낸다.

 

이를 원하는 형태로 View시키기 위해 아래와 같은 DTO객체를 사용했고, DTO에 원하는 정보를 tuple에서 뽑아서 List형태로 반환시켜 줄 계획이기 때문에 아래처럼 DTO와 반환 코드를 작성했다.

 

package com.example.lolchampionsinvestment.domain.champion;

import lombok.Data;

@Data
public class ChampionPriceDto {

    private String name;

    private int price;

    private int percent;

    public ChampionPriceDto(String name, int price, int percent) {
        this.name = name;
        this.price = price;
        this.percent = percent;
    }
}
List<ChampionPriceDto> championPriceDtoList = tupleList.stream()
        .map(tuple -> {
            String name = tuple.get(C.name);
            int price = tuple.get(C.price);
            int cplPrice = tuple.get(CPL.price);
            int percent = Math.round((price - cplPrice) * 100 / price);
            return new ChampionPriceDto(name, price, percent);
        })
        .collect(Collectors.toList());

 

 

select C.CHAMPION_ID, C.PRICE, ROUND((C.PRICE-CP.PRICE)/CP.PRICE*100) as PERCENT
  from Champion C, (select CC.CHAMPION_ID, CC.PRICE
					  from ChampionPriceLog CC
					 where (CC.CHAMPION_ID, CC.CREATE_DATE) in(select CCC.CHAMPION_ID, MAX(CCC.CREATE_DATE) AS CREATE_DATE
																 from ChampionPriceLog CCC
																group by CCC.CHAMPION_ID)
					order by CC.CHAMPION_ID) as CP
 where C.CHAMPION_ID = CP.CHAMPION_ID;

위의 쿼리를 QueryDSL로 수행하기 위한 테스트 코드 전체는 아래와 같다.

 

@SpringBootTest
@Transactional
public class ChampionPriceQueryRepositoryTest {

    @PersistenceContext
    EntityManager em;
    JPAQueryFactory jpaQueryFactory;

    @Autowired
    ChamiponPriceQueryRepository championPriceQueryRepository;
    @Autowired
    ChampionDataParsingService championDataParsingService;

    @BeforeEach
    void init() {
        jpaQueryFactory = new JPAQueryFactory(em);
    }

    @DisplayName("챔피언 별 가장 최근 가격을 가져온다.")
    @Test
    void getChampionsLatestPrice(){
        // given
        List<ChampionPriceDto> championPriceDtoList = championPriceQueryRepository.findAllLatestChampions();

        // when // then
        assertThat(championPriceDtoList.size()).isEqualTo(164);

        assertThat(championPriceDtoList)
                .extracting("name", "price", "percent")
                .containsAnyOf(
                        tuple("아트록스", 7000, -36),
                        tuple("아리", 2000, 0),
                        tuple("카이사", 8000, 0)
                );
    }

}

 

테스트를 위해, 아트록스 데이터는 11000원을 입력해두었다.

원하는 값인 현재 금액 : 7000, 금일 등락 퍼센트인 -36%가 잘 반환된 모습이다.

 

 

 

 

적용

테스트코드를 적용시킨 Repository를 만들고

 

아래의 서비스 객체를 만든 뒤

@Service
@RequiredArgsConstructor
@Transactional
public class ChampionService {

    private final ChamiponPriceQueryRepository championPriceQueryRepository;

    public List<List<ChampionPriceDto>> getAllLatestChampions() {
        List<ChampionPriceDto> list = championPriceQueryRepository.findAllLatestChampions();
        int listSize = list.size();

        List<List<ChampionPriceDto>> groupingList = new ArrayList<>();
        for(int i = 0; i < listSize; i+=8) {
            int endIdx = Math.min(i + 8, listSize);
            groupingList.add(list.subList(i, endIdx));
        }

        return groupingList;
    }
}

 

Controller를 통해 View를 시켜 보았다.

@RestController
public class MainViewController {

    @Autowired
    ChampionService championService;

    @GetMapping("/")
    public ModelAndView mainView() {
        ModelAndView mv = new ModelAndView();
        List<List<ChampionPriceDto>> groupingList = championService.getAllLatestChampions();

        mv.addObject("groupingList", groupingList);
        mv.setViewName("main/main.html");
        return mv;
    }

}

 

8개씩 그룹핑 한 이유는, View를 그렇게 하기 위함이었는데

일단 원하는 대로 잘 들어오긴 했고, 모든 데이터가 다 들어오기 때문에, 메인 화면에 보여주는 것이다 보니 등락폭이 큰 녀석들만 추려서 View시키던, 페이징을 시키는 방향으로 보수작업을 할 것 같다.

 

 

 

 

관련 포스팅

QueryDSL이란? - QueryDSL 사용하기(1)

JPQL이란? - QueryDSL 사용하기(2)

QueryDSL JPA Test (Spring Boot 3.0) - QueryDSL 사용하기(3)

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

QueryDSL JPA Test (Spring Boot 3.0) - QueryDSL 사용하기(3)

Tech/Java & Spring 2023. 8. 3. 07:52
728x90
728x90

셋팅

def queryDslVersion = '5.0.0'

dependencies {
	//QueryDsl
    // 필수
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
	implementation "com.querydsl:querydsl-core:${queryDslVersion}"
    
    // QueryDsl 쿼리 타입 생성 (QClass 생성 시 @Entity 탐색)
	annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
    
    // java.lang.NoClassDefFoundError:javax/persistence/Entity 에러 방지
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
}

// QueryDsl 빌드 옵션 (선택)
// QueryDsl 디렉토리 경로
def querydslDir = "$buildDir/generated/querydsl"

// 경로 추가 >> QueryDsl 소스 코드 컴파일 시 빌드
sourceSets {
	main.java.srcDirs += [ querydslDir ]
}

// 컴파일 설정(AnnotationProcessor가 생성하는 소스코드를 해당 경로로 설정)
tasks.withType(JavaCompile) {
	options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

// clean실행 시 마지막 작업으로 디렉토리(QClass) 삭제 >> 충돌 방지
clean.doLast {
	file(querydslDir).deleteDir()
}

 

 

 

위와 같은 셋팅을 사용했고, 주의할 점은 아래에 있다.

implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"

//implementation 'com.querydsl:querydsl-jpa'

아래처럼, 버전과 jakarta를 명시해주지 않으면 Config파일 설정 시 에러가 발생한다.

 

 

또한 Q 파일을 찾지 못해 java.lang.NoClassDefFoundException 관련 에러가 발생한다면 아래 코드를 추가하자.

annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

 

또한 아래의 Config 클래스를 생성하여 JPAQueryFactory를 Bean으로 등록해주었다.

필요 시 Repository에서 JPAQueryFactory를 DI하여 사용할 수 있다.

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
 
    private final EntityManager em;
    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager em){
        return new JPAQueryFactory(em);
    }
}

 

 

모든 설정을 마쳤다면 Gradle - Build - Clean 후 compileJava를 수행하거나. (Gradle 빌드 도구 활용)

 

직접 터미널에 아래 명령어를 입력해도 된다.

./gradlew clean

./gradlew compileJava

 

 

 

테스트 해보기

package com.example.lolchampionsinvestment;

import com.example.lolchampionsinvestment.config.QuerydslConfig;
import com.example.lolchampionsinvestment.domain.champion.Champion;
import com.example.lolchampionsinvestment.domain.champion.ChampionRepository;
import com.example.lolchampionsinvestment.domain.champion.QChampion;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;

import java.time.LocalDateTime;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@ActiveProfiles("test")
@Import(QuerydslConfig.class)
@Transactional
public class QuerydslTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory jpaQueryFactory;

    @BeforeEach
    void init() {
        jpaQueryFactory = new JPAQueryFactory(em);

        Champion aatrox = createChampion("Aatrox", 1000, "아트록스", LocalDateTime.now());
        Champion teemo = createChampion("Teemo", 2000, "티모", LocalDateTime.now());
        Champion ahri = createChampion("Ahri", 3000, "아리", LocalDateTime.now());
        em.persist(aatrox);
        em.persist(teemo);
        em.persist(ahri);
    }

    @DisplayName("Querydsl test >> 올바르게 insert되었는지 확인")
    @Test
    void selectBeforeInsertDataWithQuerydsl(){
        //given
        QChampion champion = new QChampion("champion");

        //when
        Champion result1 = jpaQueryFactory.select(champion)
                .from(champion)
                .where(champion.name.eq("Aatrox"))
                .fetchOne();
        Champion result2 = jpaQueryFactory.select(champion)
                .from(champion)
                .where(champion.name.eq("Teemo"))
                .fetchOne();
        Champion result3 = jpaQueryFactory.select(champion)
                .from(champion)
                .where(champion.name.eq("Ahri"))
                .fetchOne();

        //then
        assertThat(result1.getName()).isEqualTo("Aatrox");
        assertThat(result2.getPrice()).isEqualTo(2000);
        assertThat(result3.getDescription()).isEqualTo("아리");
    }

    private static Champion createChampion(String name, int price, String description, LocalDateTime createDateTime) {
        return Champion.builder()
                .name(name)
                .price(price)
                .description(description)
                .createDateTime(createDateTime)
                .build();
    }


}

 

테스트는 정상 수행되었다.

 

Insert 세 건에 대한 조회를 진행하여 모두 일치하는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

다양한 쿼리 키워드나, 연산에 대한 사용법은 정리가 잘 된 포스팅을 발견했다. 결국 API를 호출해서 API의 포맷대로 잘 사용하면 되는 것 같다.

 

 

Spring-Data-JPA [8] Querydsl 사용

저번 포스팅에선 Querydsl 설정을 했습니다. 이제는 직접 사용해보겠습니다. 1. JPAQueryFactory 등록 JpaRepository를 custom 했다는 컨벤션으로 ~~ RepositoryCustom을 만들어 해당 Repository를 상속하고, ~~ Impl로

dingdingmin-back-end-developer.tistory.com

 

 

 

 

마무리

다음 포스팅에선, 드디어 실제 토이 프로젝트에 적용될 예시 쿼리를 QueryDSL로 직접 사용해서 View까지 해보도록 하겠다.

 

 

 

 

 

 

 

참조

https://velog.io/@youngerjesus/%EC%9A%B0%EC%95%84%ED%95%9C-%ED%98%95%EC%A0%9C%EB%93%A4%EC%9D%98-Querydsl-%ED%99%9C%EC%9A%A9%EB%B2%95

https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/

https://binco.tistory.com/entry/QueryDSL-%EC%A0%81%EC%9A%A9-%EC%98%88%EC%A0%9C%EC%8B%9C%EB%A6%AC%EC%A6%88

https://velog.io/@soyeon207/QueryDSL-Spring-Boot-%EC%97%90%EC%84%9C-QueryDSL-JPA-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

 

 

 

 

 

관련 포스팅

QueryDSL이란? - QueryDSL 사용하기(1)

JPQL이란? - QueryDSL 사용하기(2)

 

 

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

QueryDSL이란? - QueryDSL 사용하기(1)

Tech/Java & Spring 2023. 8. 2. 07:28
728x90
728x90

서론

토이 프로젝트 진행 중 아래와 같은 쿼리 사용이 필요했고, QueryDSL을 한 번 사용해보기로 했음.

select C.CHAMPION_ID, C.PRICE, ROUND((C.PRICE-CP.PRICE)/CP.PRICE*100) as PERCENT
  from Champion C, (select CC.CHAMPION_ID, CC.PRICE
					  from ChampionPriceLog CC
					 where (CC.CHAMPION_ID, CC.CREATE_DATE) in(select CCC.CHAMPION_ID, MAX(CCC.CREATE_DATE) AS CREATE_DATE
																 from ChampionPriceLog CCC
																group by CCC.CHAMPION_ID)
					order by CC.CHAMPION_ID) as CP
 where C.CHAMPION_ID = CP.CHAMPION_ID;

 

 

 

QueryDSL

간략하게 QueryDSL에 대한 조사를 했다.

 

 

Querydsl - Unified Queries for Java

Unified Queries for Java. Querydsl is compact, safe and easy to learn. <!-- Querydsl Unified Queries for Java Querydsl provides a unified querying layer for multiple backends in Java. Compared to the alternatives Querydsl is more compact, safer and easier

querydsl.com

 

QueryDSL이란 하이버네이트 쿼리 언어의 쿼리를 생성 및 관리해주는 오픈소스 프레임워크이며

위의 공식 문서에서 알 수 있듯이 JPA뿐 아니라 여러 언어를 지원한다.

 

QueryDSL은 정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해준다.

 

 

 

QueryDSL JPA

진행중인 토이 프로젝트가 스프링부트를 사용하기 때문에, QueryDSL JPA에 대해 알아볼 필요가 있다.

 

QueryDSL JPA는 SQL, JPQL을 코드로 작성할 수 있도록 해주는 빌더 API이며, Entity클래스와 매핑되는 QClass라는 객체를 사용해 쿼리를 실행한다.

 

 

JPQL 이란?

공부를 위해 포스팅해놓은 링크가 있으니 참조하자.

 

 

QClass 란?

위에서 언급했든 QueryDSL은 컴파일 단계에서 Entity를 매핑하여 QClass를 생성한다.

 

JPAAnnotationProcessor 클래스(docs 링크)가 컴파일 시점에 작동하여 @Entity 애너테이션을 찾아 분석하여 Entity와 형태가 같은 Static Class로 QClass를 생성하며, QueryDSL은 쿼리 작성 시 이 QClass를 기반으로 쿼리를 실행하게 된다.

 

 

 

QueryDSL JPA 사용 이유

그렇다면 QueryDSL을 왜 사용할까?

 

JPQL은 결국 쿼리를 문자열로 입력하기 때문에, 오타가 발생하거나 관리하기가 어렵고, 컴파일 단계에서가 아닌 런타임 시 오류를 확인할 수 있다는 문제가 있다.

 

QueryDSL은 단순 문자열이 아닌 코드를 통해 작성하기 때문에, 코드 작성자의 실수(오타)를 예방할 수 있으며, 객체 지향적인 개발이 가능하다. 또한 컴파일 단계에서도 오류를 빠르게 발견할 수 있다는 장점이 있다.

 

 

아래의 예시는 JPQL과 QueryDSL의 같은 상황에 대한 사용 예시이다.

String jpql = "select * from Member m join Point p on p.member_id = m.id"
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
return jpaQueryFactory
.from(member)
.join(member.point, point)
.fetch();

 

JPQL이 객체지향 쿼리라고는 하지만, 결국 String 쿼리를 객체로 변환시켜주는 과정이 필요하기 때문에, 문자열 입력 시 오타와 같은 상황이 충분히 발생 가능하며, 문자열이 잘못됐더라도 컴파일 단계에서는 확인이 불가능한 것을 알 수 있다.

 

QueryDSL은 쿼리를 문자열이 아닌, 코드화하여 전달하기 때문에 위에서 언급했던 장점들을 가져갈 수 있다.

 

 

 

 

 

 

 

 

참조

https://ittrue.tistory.com/292

https://velog.io/@soyeon207/QueryDSL-%EC%9D%B4%EB%9E%80

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

JPQL이란? - QueryDSL 사용하기(2)

Tech/Java & Spring 2023. 8. 1. 19:28
728x90
728x90

서론

이전 글에 나왔던 키워드 중 JPQL에 대해 조금이라도 알아보기 위해 정리하는 글

 

 

JPQL(Java Persitence Query Language)이란?

엔티티 객체를 대상으로 하는 객체지향 쿼리로 SQL을 추상화한 객체지향 쿼리이며, 작성된 JPQL은 SQL로 변환된다.

 

기존 JPA의 메서드 호출만으로는 섬세한 쿼리 작성이 어렵다는 문제를 해결하기 위해 JPQL이 나타나게 되었으며 SQL을 추상화했기 때문에 특정 데이터베이스 SQL에 의존하지 않는다는 장점이 있다.

 

SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN을 지원한다.

 

 

 

예시

Member 객체를 대상으로 이름에 "bazzi"가 포함된 모든 회원을 검색하는 쿼리

String jpql = "select m From Member m where m.name like ‘%bazzi%'";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();

for (Member member : result) {
    System.out.println("Member = " + member);
}

 

 

SQL과의 차이점

1. 대소문자 구분

엔티티와 필드는 대소문자를 구분한다.

위의 예시에서는, Member와 name은 대소문자를 반드시 구분해주어야 한다.

select, from, as와 같은 키워드들은 대소문자를 구분하지 않아도 된다.

 

2. 엔티티 이름

Member은 Entity 이름으로, @Entity(name = "Menber")로 설정 가능하며, 생략 시 기본 값으로 클래스 이름을 사용한다.

 

3. 별칭(Alias)

JPQL에서 Alias는 필수로 명시해야 하며, AS 키워드는 생략 가능하다.

 

 

구현 방법

구현 방법에는 TypedQuery, Query가 있다.

 

TypedQuery는 변환 타입이 명확할 때, 아래처럼 타입 정보를 작성하여 리턴받을 수 있다.

TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);

기본적으로 엔티티 타입을 기입했고, query2와 같이 리턴 타입이 username과 같이 명확할 경우 타입 정보를 기입할 수 있다.

 

 

 

Query는 데이터 검색 결과의 타입을 명시하지 않는다.

 

Query query3 = em.createQuery("select m.username, m.age from Member m");

 

 

조회한 결과는 getResultList()혹은 getSingleResult()와 같은 API를 사용하여 List 혹은 단일 객체를 반환한다.

TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query1.getResultList();

for (Member m : resultList) {
    System.out.println("m = " + m.getUsername());
}
TypedQuery<Member> query2 = em.createQuery("select m from Member m where m.id = 1L", Member.class);
Member singleResult = query2.getSingleResult();

System.out.println("singleResult = " + singleResult.getUsername());

 

 

 

파라미터 바인딩

이름 기준 파라미터

Member result = em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", "member1")
                .getSingleResult();

System.out.println("result = " + result.getUsername());

이름을 기준으로 파라미터를 바인딩하며, 콜론(:)을 사용해 데이터가 추가될 곳을 지정하며

query.setParameter()을 호출하여 데이터를 동적으로 바인딩한다.

 

 

위치 기준 파라미터

Member result = em.createQuery("select m from Member m where m.username = ?1", Member.class)
                .setParameter(1, "member1")
                .getSingleResult();

System.out.println("result = " + result.getUsername());

위치 기준 파라미터를 사용하려면 ? 다음에 위치 값을 주면 된다.

 

위치가 변경될 가능성이 높기 때문에, 위치 기준 파라미터보다는 이름 기준 파라미터를 사용하는 것을 권장한다고 한다.

 

 

 

JPQL의 문제점

객체지향 쿼리이지만, 문자열을 통해 JPQL을 작성하는 것이기 때문에 오타가 발생하기 쉽고, 유지보수가 어렵다.

 

또한 문자열 자체의 검증이 컴파일 단계에서 불가능하기 때문에, 쿼리가 직접 실행되는 런타임에서 해당 오류를 발견할 수 있다. 물론 이는 테스트 코드에서 해소할 수 있는 부분이 있지만, 실제 운영 시에도 발생할 수 있다는 불안을 해소할 수 없다.

 

 

 

 

 

 

 

 

 

참조

https://dev-coco.tistory.com/141

https://ittrue.tistory.com/270

https://ittrue.tistory.com/292

https://velog.io/@soyeon207/QueryDSL-%EC%9D%B4%EB%9E%80

 

 

 

 

 

 

관련 포스팅

QueryDSL란? - QueryDSL 사용하기(1)

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[Java] BufferedReader BufferedWriter

Tech/Java & Spring 2023. 6. 6. 06:56
728x90
728x90

서론

요새 프로그래머스가 잠잠해져서 LeetCode 외에도 HackerRank라는 플랫폼에서도 코테문제를 풀고있는데, 얼른 플랫폼에 적응해서 적당한 난이도의 문제를 풀고 포스팅 하고 싶다

 

 

HackerRank - Online Coding Tests and Technical Interviews

HackerRank is the market-leading technical assessment and remote interview solution for hiring developers. Start hiring at the pace of innovation!

www.hackerrank.com

 

서론이 이상하게 흘러갔는데 여튼, 백준처럼 직접 입력받아 출력해야하는 포맷의 HackerRank인 만큼, 입출력에 대한 공부를 조금더 할 필요가 있었다.

 

 

 

BufferedReader / BufferedWriter

기존 입출력을 Scanner와 System.out을 활용하는 것에 익숙해져 있었고

요새는 코딩을 할 때, 파일 입출력 외에는 입출력을 사용하지 않아 거의 가물가물..

 

코테 문제를 풀 때, String값을 자주 바꿔줘야할 때 아래의 예시처럼, String에 직접 값을 추가하는 것 보다 StringBuffer의 append()를 활용하는 것이 성능 테스트 시 속도가 더 빠른 것을 경험한 적이 있다.

String str = "";
str += "a";
str += "pp";
str += "le";

StringBuffer str = new StringBuffer();
str.append("a");
str.append("pp");
str.append("le");

 

입력된 데이터가 버퍼를 거쳐 전달되어 데이터 처리 효율성이 높다. 즉 속도가 훨씬 빠르다. 그래서 많은 양의 데이터를 처리할 때 유리하며, 코테 문제에서도 StringBuffer을 사용하는 것이 더 좋다. 라고 하는 것이다.

 

 

BufferedReader의 기본 사용법은 아래와 같다.

BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));

String str = bf.readLine();
int num = Integer.parseInt(bf.readLine());

 

입력 시에는 readLine()을 활용하며, 아래의 int 선언구와 같이 String이 아닌 다른 타입으로 사용 시 형변환이 필수적이다. 또한 예외처리가 필수적이며, IDE를 사용 시 readLine을 사용하다 보면 IOException에 대한 예외처리를 해달라고 컴파일 에러를 낸다. 말그대로 Input / Output에 대한 Exception이다.

 

 

IOException (Java Platform SE 8 )

Constructs an IOException with the specified detail message and cause. Note that the detail message associated with cause is not automatically incorporated into this exception's detail message.

docs.oracle.com

 

 

또한 위의 예시에서처럼, Line단위로 구분을 짓기 때문에, 가공을 위해서 후처리를 해주어야 한다.

StringTokenizer st = new StringTokenizer(str);
int num1 = Integer.parseInt(st.nextToken());
int num2 = Integer.parseInt(st.nextToken());

String str[] = str.split(" ");

StringTokenizer에 입력받은 문자열을 넣어, nextToken()을 활용하여 입력받은 줄 단위의 문자열에서 공백 단위로 구분지어 순서대로 호출할 수 있다. 또한 split을 사용하는 것도 방법이 될 수 있다.

 

그리고, 버퍼를 사용하여 데이터를 읽기 때문에 버퍼에 남아있는 데이터를 소비하고 해제하여야 하며 이를 위해 close 메서드를 마지막에 사용해 주는 것이 좋다.

bf.close();

 

 

 

다음으로 BufferedWriter을 사용하는 예제이다.

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String str = "apple";
bw.write(str+"\n");
bw.flush();
bw.close();

 

우선 flush()와 close()가 눈에 띄는데, 객체 생성 시 새로운 버퍼가 생성되기 때문에, flush()를 사용하여 버퍼에 남아있는 데이터를 강제로 출력하고, 버퍼를 비워주어야하며, close()를 사용하여 버퍼를 닫고 리소스를 해제하는 후처리 작업이 동행되어야 한다.

 

 

 

 

예제

서론에서 언급했던 HackerRank 예제이다

 

Simple Array Sum | HackerRank

Calculate the sum of integers in an array.

www.hackerrank.com

import java.io.*;
import java.math.*;
import java.security.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.regex.*;
import java.util.stream.*;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

class Result {

    /*
     * Complete the 'simpleArraySum' function below.
     *
     * The function is expected to return an INTEGER.
     * The function accepts INTEGER_ARRAY ar as parameter.
     */

    public static int simpleArraySum(List<Integer> ar) {
    // Write your code here

    }

}

public class Solution {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(System.getenv("OUTPUT_PATH")));

        int arCount = Integer.parseInt(bufferedReader.readLine().trim());

        List<Integer> ar = Stream.of(bufferedReader.readLine().replaceAll("\\s+$", "").split(" "))
            .map(Integer::parseInt)
            .collect(toList());

        int result = Result.simpleArraySum(ar);

        bufferedWriter.write(String.valueOf(result));
        bufferedWriter.newLine();

        bufferedReader.close();
        bufferedWriter.close();
    }
}

 

단순 배열안에 있는 모든 값을 더하는 문제였고 아래와 같이 해결함.

public static int simpleArraySum(List<Integer> ar) {
    int answer = 0;
    for(int i : ar){
        answer += i;
    }
    return answer;
}
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

SynchronizedList / CopyOnWriteArrayList

Tech/Java & Spring 2023. 6. 2. 06:16
728x90
728x90

[Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor)

연관 게시물 https://mag1c.tistory.com/364 스레드 안전 - Thread Safe 연관 게시물 https://mag1c.tistory.com/365 [Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor) 자바에서의 Thread-Safe 1. Lock synchronized 아래

mag1c.tistory.com

스레드 안전 - Thread Safe

연관 게시물 https://mag1c.tistory.com/365 [Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor) 자바에서의 Thread-Safe 1. Lock synchronized 아래 코드는 Synchronized 키워드를 사용하여 스레드의 안전성을 보

mag1c.tistory.com

 


 
 
 
멀티 쓰레드 환경에서, List를 사용하기 위해 SynchronizedList나 CopyOnWriteArrayList를 사용할 수 있다.
 
 

SynchronizedList

java.utils.Collections의 List를 상속받아 구현되어있으며, synchronized 키워드를 통해 thread-safe를 구현시킨다.

// java.util.Collections

...

static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

    ...

    public E get(int index) {
        synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }

    public int indexOf(Object o) {
        synchronized (mutex) {return list.indexOf(o);}
    }
    public int lastIndexOf(Object o) {
        synchronized (mutex) {return list.lastIndexOf(o);}
    }

    ...
}

 
 
 

CopyOnWriteArrayList

set, add과정에서 lock과 함께 array를 copy후 write하는 방식으로 처리하기 때문에
get을 수행할 때 thread-safe하게 조회가 가능하다.

// java.util.concurrent.CopyOnWriteArrayList

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    final transient Object lock = new Object();

    private transient volatile Object[] array;

    ...

    public E get(int index) {
        return elementAt(getArray(), index);
    }

    public E set(int index, E element) {
        synchronized (lock) {
            Object[] es = getArray();
            E oldValue = elementAt(es, index);

            if (oldValue != element) {
                es = es.clone();
                es[index] = element;
            }
            setArray(es);
            return oldValue;
        }
    }

    public boolean add(E e) {
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            setArray(es);
            return true;
        }
    }

    ...
}

SynchronizedList는 단순 synchronized 키워드를 사용한 동기화된 List이기 때문에 모든 읽기, 쓰기 동작에 lock이 걸린다. 때문에 읽기보다 쓰기 동작이 많고, 크기가 큰 리스트의 경우에 적합할 것이다.
 
CopyOnWriteArrayList는 set, add과정에서 lock과 함께 copy후 write하는 방식이기 때문에, 읽기 동작에서의 수행능력이 좋을 것이다. 또한 특유의 동작 방식때문에 쓰기 동작에서는 상당한 부하가 발생할 것이다.

 

 
 

SynchronizedList : read < write
CopyOnWriteArrayList : read > write

 
 
 
 
 
 
 
 
 

참조
https://taes-k.github.io/2021/12/26/synchronizedlist-copyonwritelist/
https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#synchronizedList-java.util.List-
http://asuraiv.blogspot.com/2020/02/java-synchronizedlist-vs.html
https://jihyehwang09.github.io/2020/04/14/java-copyonwritearraylist/
 
 
 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

ConcurrentHashMap이란? HashMap vs HashTable vs ConcurrentHashMap

Tech/Java & Spring 2023. 5. 31. 05:51
728x90
728x90

Map의 인터페이스의 구현체로는 HashMap, HashTable, ConcurrentHashMap등이 있는데
thread-safe와 hashtable관련 포스팅을 했기 때문에 어떤 경우에 어떤 자료구조를 사용해야 하는지 알아보려 한다.

[자료구조] 해시테이블 (hashtable)

해시테이블 (hashtable) Key, Value 로 데이터를 저장하는 자료구조 중 하나이며 데이터를 빠르게 검색할 수 있는 자료구조이다. 빠른 검색을 할 수 있는 이유는 내부적으로 버킷(배열)을 사용하여 데

mag1c.tistory.com

[Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor)

연관 게시물 https://mag1c.tistory.com/364 스레드 안전 - Thread Safe 연관 게시물 https://mag1c.tistory.com/365 [Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor) 자바에서의 Thread-Safe 1. Lock synchronized 아래

mag1c.tistory.com

스레드 안전 - Thread Safe

연관 게시물 https://mag1c.tistory.com/365 [Java] 자바에서의 스레드 안전(Thread Safe)과 모니터(monitor) 자바에서의 Thread-Safe 1. Lock synchronized 아래 코드는 Synchronized 키워드를 사용하여 스레드의 안전성을 보

mag1c.tistory.com

 
 
 

HashMap

Key Value에 null을 허용하지만, 동기화를 보장하지 않는다.
동기화 처리를 하지 않기 때문에 데이터 탐색 속도는 빠르지만, 신뢰성과 안정성이 떨어진다.
 
 
 

HashTable

Key Value에 null을 허용하지 않고, 동기화를 보장한다.

get() put() remove()등의 메서드에 synchronized 키워드가 붙어 있어, 멀티 쓰레드 환경에서 사용할 수 있다.
synchronized 키워드 때문에 동작이 느리다. (쓰레드 간 동기화 Lock 때문에...)
 
 
 
 

ConCurrentHashMap

Key Value에 null을 허용하지 않으며 동기화를 보장한다.
동기화 처리 시, 조작하는 버킷(index)에 대해서만 락을 걸기 때문에, 같은 멀티 쓰레드 환경에서 사용하더라도 HashTable 대비 속도가 빠르다.

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

    public V get(Object key) {}

    public boolean containsKey(Object key) { }

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
}

 
ConcurrentHashMap클래스의 일부 API이다. Hashtable과 달리 synchronized 키워드가 메서드 전체에 붙어있지 않다. get() 은 아예 존재하지도 않는다. put() 메서드에는 중간에 synchronized 키워드가 있다.
 
즉 ConcurrentHashMap은 읽기 작업에는 여러 쓰레드가 동시에 읽을 수 있지만, 쓰기 작업에서는 특정 버킷에 대한 Lock을 사용한다는 것을 의미한다.
 

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

    private static final int DEFAULT_CAPACITY = 16;

    // 동시에 업데이트를 수행하는 쓰레드 수
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
}

 
DEFAULT_CAPACITY, DEFAULT_CONCURRENCY_LEVEL이 16으로 설정되어 있는 것을 볼 수 있다.
DEFAULT_CAPACITY는 버킷의 수이며, DEFAULT_CONCURRENCY_LEVEL는 동시에 작업 가능한 쓰레드 수라고 생각한다.
 
버킷의 수가 동시작업 가능한 쓰레드의 수인 이유는 버킷 단위로 lock을 사용하기 때문에 같은 버킷만 아니라면 Lock을 기다릴 필요가 없다. 동시에 데이터를 삽입, 참조하더라도 그 데이터가 다른 버킷에 위치하면 서로 락을 얻기위해 경쟁하지 않는다는 뜻이다.
 
위의 특징들을 바탕으로 정리해 볼 때, ConcurrentHashMap은 읽기보다는 쓰기 작업에서, 성능이 중요한 상황에서 쓰는 것이 적합하다. 같은 버킷이 아니라면 여러 쓰레드가 동시에 삽입하는 것이 가능하기 때문이다.
 
 
 
 

코드 테스트

위의 컬렉션들을 10개의 스레드에서 각각 1000번을 반복하여 랜덤 값을 입력할 때 엔트리의 사이즈를 비교하는 코드이다. (성능 테스트가 아님) (코드테스트 출처)

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MultiThreadsTest {

	private static final int MAX_THREADS = 10;

	private static Hashtable<String, Integer> ht = new Hashtable<>();
	private static HashMap<String, Integer> hm = new HashMap<>();
	private static HashMap<String, Integer> hmSyn = new HashMap<>();
	private static Map<String, Integer> hmSyn2 = Collections.synchronizedMap(new HashMap<String, Integer>());
	private static ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();

	public static void main(String[] args) throws InterruptedException {

		ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS);

		for( int j = 0 ; j < MAX_THREADS; j++ ){
			es.execute(new Runnable() {
				@Override
				public void run() {
					for( int i = 0; i < 1000; i++ ){

						String key = String.valueOf(i);

						ht.put(key, i);
						hm.put(key, i);
						chm.put(key, i);
						hmSyn2.put(key, i);

						synchronized (hmSyn) {
							hmSyn.put(key, i);
						}
					}
				}
			});
		}

		es.shutdown();
		try {
			es.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		System.out.println("Hashtable size is "+ ht.size());
		System.out.println("HashMap size is "+ hm.size());
		System.out.println("ConcurrentHashMap size is "+ chm.size());
		System.out.println("HashMap(synchronized) size is "+ hmSyn.size());
		System.out.println("synchronizedMap size is "+ hmSyn2.size());

		/*
		for( String s : hm.keySet() ){
			System.out.println("["+s+"] " + hm.get(s));
		}
		*/
	}
}
Hashtable size is 1000
HashMap size is 1281
ConcurrentHashMap size is 1000
HashMap(synchronized) size is 1000
synchronizedMap size is 1000

 
HashMap은 동기화가 이루어지지 않아 엔트리 사이즈가 비정상적으로 나오지만 HashMap을 synchronized 키워드를 사용하면 정상적으로 동작한다.
 
동기화 이슈가 있다면 일반적인 HashMap을 사용하지 말고, 동기화를 보장하는 컬렉션 또는 synchronized 키워드를 사용해 반드시 동기화 처리를 해주어야 한다.
 
 
 

요약

 HashMapHashTableConcurrentHashMap
nullableOXX
thread-safeXOO
recommandsingle-threadmulti-threadmulti-thread

 
 
 
 
 
참조
https://jdm.kr/blog/197 
https://tecoble.techcourse.co.kr/post/2021-11-26-hashmap-hashtable-concurrenthashmap/ 
https://baeldung.com/java-synchronizedmap-vs-concurrenthashmap
https://pplenty.tistory.com/17 
https://devlog-wjdrbs96.tistory.com/269 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[Project 회고록] 친구 추가 관련 기능 구현 시 테이블에 대한 고찰(친구 목록과 친구 요청에 대한 테이블을 나눌 것인가?)

Tech/Java & Spring 2023. 5. 30. 00:01
728x90
728x90

개요

 

 

위 사진은 현재 개발중인 토이 프로젝트의 User - Friend의 테이블 구조이다.
 
친구 요청을 수락하여 서로 친구인 사용자 간의 개인 채팅방을 이용 가능하게 할 생각이었으며
 
친구 요청과 응답에 대한 프로세스를 하나의 테이블로 관리하고, 서로 친구가 된 친구목록을 또다른 테이블에 관리하려고 하였다.
 

(여기서 말하는 친구 요청에 대한 응답은, 웹소켓으로 구현했음)

 
 

현재 요청과 응답에 대한 프로세스

또한 요청과 응답을 관리하는 테이블의 STATUS 컬럼의 도메인값을 NOREAD, READ, REJECT, ACCEPT 의 네 경우를 두고, 요청을 보낸 사용자가 친구 요청을 받은 사용자가 친구 요청을 보지 않았는지, 봤는데 수락도 거절도 아닌지, 거절했는지에 대한 상황을 View할 수 있게 설계했다. 수락한 요청은 바로바로 FriendList에 테이블의 row로 추가할 예정이었다.
 
예를 들면 IDX가 3인 사용자가 IDX가 1인 사용자에게 요청을 보냈을 때,

 

위와 같은 row가 insert 될 것이다.
 
 
 
위의 예시처럼, IDX가 3인 사용자 A가 친구 요청 상황에 대한 조회를 원할 경우에, 아래와 같은 테이블의 상황이라고 가정하고, 내가 원하는 비즈니스 로직을 생각해 보았다.
 

 
A가 친구 추가를 보낸 것들에 대한 데이터를 요청할 때
1. A의 IDX를 가지고 FriendReqRes 테이블로 Go!!
2. IDX에 대한 요청이 있는지 Select
3. 있다면 어떤 유저에게 보냈는지(TO), 현재 A가 보낸 요청에 대한 상태(STATUS)를 List<Entity>에 담기
4. View 시켜주기
 
또한 해당 유저가 상대방에게 다시 친구 요청을 할 수 있는 상황은 상대방이 거절했을 때
즉 STATUS = REJECT일 때 라고 정책을 세웠음.
 
마지막으로 ACCEPT에 대한 row는 FriendList로 insert되며 상대방이 수락하여 서로 친구인 상황이기 때문에
FriendList에 아래와 같이 추가시켜 줄 생각이었다.
 

 

여기서 COMMENT 이하 컬럼값들은 생각하지말고, USER_IDXFRIEND_IDX만 살펴보자.
서로 친구인 상황이기 때문에, IDX가 3의 친구목록 뿐 아니라, IDX가 4인 친구목록에도 서로가 있어야 한다.
위의 row 하나만 추가할 경우, 서로가 친구인 상황을 DB를 조회하여 얻어내기 힘들기 때문에 친구 요청을 수락한 경우, FriendList 테이블에 두 개의 row를 insert하는 것이 맞다고 판단했다.
 
 
 

 
 

의문점

FriendList 테이블 하나에서 관리해도 되지 않을까??? 라는 의문이 들었다.
 
위와 같이, A라는 유저가 다시 IDX가 4인 사용자에게 친구 요청을 보냈다고 가정하자.
 

 

위와 같은 테이블의 row가 insert될 것이다.
4번 유저가 요청을 읽었을 때, 두 테이블의 row 모두 READ로 update해주고
거절했다면 REJECT, 수락했다면 FRIEND로 변경하여 서로 친구임을 나타낼 수 있다.
 
 
 
위와 같이 한 테이블로 관리하면 되지 않을까????? 라는 생각을 했다.
 
 
두 경우를 나름 정리해보았다.
 
1. 도메인 관리를 해야하는가 : NO
  - 어짜피 두 경우 다 도메인 관리를 해야함. 하다못해 도메인값이 뭐가 뭔지 내가 알아야함
  - 여기서 말하는 도메인이란 create domain을 통한 도메인생성이 아니라, 해당 컬럼에 대한 범위 라고 정의했다.
 
2. 무결성을 고려해야 하는가 : 하긴 해야함
  - 당연히 데이터의 무결성을 고려해야한다고 생각은 하지만 두 경우 모두 대비해야함
  - IDX는 User의 PK기 때문에 중복될 일이 없다고 판단
  - 트랜잭션으로 누락관리가 가능한 코드를 이미 짰음.
 
3. 성능 및 기타 issue
  - 테이블 분할 시 조인여부 : NO(요청을 수락하면 해당 IDX들을 들고가서 FriendList에 추가만 하면 된다.)
  - 유지보수, 확장성 : 아무래도 분할하는 것이 더 낫지 않나... 라는 생각
 
결국 두 테이블을 사용하는 방법은 데이터의 일관성이나 무결성을 보장하고, 복잡성이 감소할 것이고
FriendList에 STATUS column만 추가하여 사용하면 간단하게 데이터를 관리할 수 있지만, 요청과 응답, 해당 친구의 COMMENT update 혹은 친구 삭제 등 모든 로직을 한 테이블로 수행해야하기 때문에 로직이 추가되고 추가되고 많이 추가되다보면 헷갈리고 꼬일 수 있다고 생각한다.
 
 
 

결론

그래서 어떤 방법을 사용해야 할까?
우선 테이블을 분할하여 관리하기로 했다.
 
사실 두 방법다 나쁘지 않은 것 같다. 더 좋은 방법도 있는 것 같기도 한데 생각이 나지 않음..
 
결론 쪽을 쓰면서 하나 더 생각난 것이 있는데,
 
FriendList에서 친구 요청 하나에 두 row를 생성하는 것을, 컬럼 하나를 추가하여
 
3번이 4번에게 요청 보냈을 때, 컬럼값을 3→4
수락했을 때, 해당 컬럼에 →3,
거절했다면 →X
 
이런식으로 관리하여 →로 split하여 요청에 대한 응답 여부들을 파악한다면 row가 두 배로 늘어날 일이 없지 않을까.. 라는 생각도 해보고
 
row가 엄청 늘어날 경우 일정 기간이 지난 요청과 응답들은 삭제시키거나, 로그화하여 로그테이블을 따로 만드는 것도 방법이 될 것같다.


지적 부탁드립니다!!! 개발 뉴비를 바른 길로 인도해주세요 굽신...

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록