SPARTA 과제/SPRING

Plus spring 도전과제

kjw81024 2026. 4. 6. 21:32

Level 10

queryDSL 을 사용한 검색 기능 

일정 검색하기 

 

 

일단 List 로 받아서 pageImpl 로 넘겨줘야하는데 아래와 같은 오류 발생 

Projections 쓰는 문법이 틀렸었다

constructor 안에 형태, 넣을값들 순서로 다 넣어줘야하기 때문에 select 부분을 아래와 같이 수정

.select(Projections.constructor(TodoSummaryResponse.class,
                                todo.contents,
                                todo.managers.size(),
                                todo.comments.size()))

 

-> count 가 제대로 출력이 안 된다...

List<TodoSummaryResponse> contents = factory
        .select(Projections.constructor(
                TodoSummaryResponse.class,
                todo.title,
                JPAExpressions
                        .select(manager.count())
                        .from(manager)
                        .where(manager.todo.eq(todo)),
                JPAExpressions
                        .select(comment.count())
                        .from(comment)
                        .where(comment.todo.eq(todo))
        ))
        .from(todo)
        .leftJoin(todo.managers, manager)
        .leftJoin(manager.user, user)
        .where(builder)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .distinct().fetch();

count 를 셀 때 ArrayList 의 size 를 계산할 수 없다고 함 -> JPAExpressions 를 사용해야함 

 

builder 를 사용해서 구현, 조건이 4개였는데 첫번째꺼가 좀 어려웠다 (ai 와 함께했어요...)

# 매니저들 중 한 명이라도 키워드가 포함되는지
if (StringUtils.hasText(request.getManagerName())) {
    builder.and(todo.managers.any().user.nickname.contains(request.getManagerName()));
}
# 제목 일치
if (request.getTitle() != null && !request.getTitle().isBlank()){
    builder.and(todo.title.eq(request.getTitle()));
}
# 생성일 시작점
if (request.getStartCreated() != null){
    builder.and(todo.modifiedAt.goe(request.getStartCreated()));
}
# 생성일 종료점
if (request.getEndCreated() != null){
    builder.and(todo.modifiedAt.loe(request.getEndCreated()));
}

total count 계산해주고, pageImpl 로 넘겨주고 구현

 


Level 11

# level 11 : 매니저 등록 요청 시 로그 기록
# transaction option 을 활용해서 save manager 와 logging 이 개별로 이루어질 수 있도록 하기
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Long saveInitialLog(Long loginUserId, Long todoId, Long managerId){
    ManagerLog log = new ManagerLog(loginUserId, todoId, managerId);
    ManagerLog savedLog = managerLogRepository.save(log);
    return savedLog.getId();
}

# 해당 요청이 성공했는지 실패했는지,,, 결과값도 저장
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveResultLog(Long logId, boolean status, String message){
    ManagerLog log = managerLogRepository.findById(logId).orElseThrow();
    log.addResult(status, message);
}

 

https://kjw81024.tistory.com/79

 

@Transactional 트러블슈팅

일단 Transactional option 이 뭔지 잘 모르겠어서 찾아봤다더보기참고자료 https://hstory0208.tistory.com/entry/Spring-Transactional-%EC%98%B5%EC%85%98-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%A0%84%ED

kjw81024.tistory.com

트러블 슈팅이 생겼다.... 백년만인듯 (과장)


Level 13

진짜 죽어도 배포 하기 싫어서 13번부터 풀었다 ㅋㅋ ㅎㅎ

 

아래처럼 구분해서 test1 vs test2 비교

test 3, 4 5 비교하면 될 듯 하다 

@SpringBootTest
public class MassDataProcessingTest {
    @Test
    @DisplayName("test1: 1번 조회 - 아무런 추가 없음")
    void test_case01(){}

    @Test
    @DisplayName("test2: 1번 조회 - nickname 에 idx 추가")
    void test_case02(){}

    @Test
    @DisplayName("test3: 여러번 조회 - 아무런 추가 없음")
    void test_case03(){}

    @Test
    @DisplayName("test4: 여러번 조회 - nickname 에 idx 추가")
    void test_case04(){}

    @Test
    @DisplayName("test5: 여러번 조회 - nickname 에 idx 추가, 캐싱")
    void test_case05(){}
}

 

일단 유저 데이터 500만건을 생성 - > JDBC 를 활용해서 Bulk Insert 를 진행한다

@Autowired
private JdbcTemplate jdbcTemplate;

 

 

JDBC 랑 JPA 차이를 모르겠어서 정리해두기

JPA JDBC Template 
메모리 부하가 크고 배치가 까다로움 메모리 효율이 좋고 Batch 처리에 최적화
안정적이지만 느림 빠름 

직접 SQL 문을 작성해서 보내는 형태 

 

매번 500만건을 만들어주긴 너무 오래걸릴 거 같아서 일단 @BeforeAll 사용

@BeforeAll
void initData() {
    # 테이블 초기화 및 스키마 설정
    jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0"); # foriegnkey 제약 풀어주기
    # 풀지 않으면 drop table 할 때 오류가 발생!
    jdbcTemplate.execute("DROP TABLE IF EXISTS users");
    jdbcTemplate.execute("CREATE TABLE users (" +
            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
            "nickname VARCHAR(255))");

            # 일정 시간이 지났을 때 한 번에 업데이트 
    for (int i = 0; i < COUNT / BATCH_SIZE; i++) {
        final int offset = i * BATCH_SIZE;
        String sql = "INSERT INTO users (nickname) VALUES (?)";

        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, "user_" + (offset + i));
            }
            @Override
            public int getBatchSize() {
                return BATCH_SIZE;
            }
        });
    }
}

@AfterAll
void clean() {
    jdbcTemplate.execute("TRUNCATE TABLE users");
}

 

기본적인 SQL 문은 아래를 따랐으며, cache Service class 를 따로 생성하여 성능을 확인하기 위해 test 1, 2 는 Test class 에 작성하고 3, 4, 5 는 Service 계층에 함수를 작성하고 호출하는 형식으로 진행

String sql = "SELECT * FROM users WHERE nickname = ? LIMIT 1";

 

idx 를 생성해주는 로직을 test 2, 4, 5에 추가하고

idx 를 모두 생성한 이후 시간 측정을 시작해서 idx 를 시작하는 시간이 결과값에 영향을 미치지 않도록 설정했다 

jdbcTemplate.execute("CREATE INDEX idx_nickname ON users(nickname)");

 

캐싱은 서비스를 분리하고 테스트코드 위에 enable caching annotation 을 추가하여 실험 

@EnableCaching
@Service
public class UserCacheService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    // "userCache"라는 이름의 저장소에 결과를 저장.
    // 동일한 nickname으로 호출되면 DB에 안 가고 캐시된 result를 바로 반환!
    @Cacheable(value = "userCache", key = "#nickname")
    public List<Map<String, Object>> getUserByNickname(String nickname) {
        String sql = "SELECT * FROM users WHERE nickname = ? LIMIT 1";
        return jdbcTemplate.queryForList(sql, nickname);
    }
}

test 1 vs test 2

인덱스 미사용 인덱스 사용
1084 ms 4 ms

한 번만 조회하는건 어차피 캐시의 성능을 확인할 수 없기 때문에 cache + idx는 단일 조회에서는 제외했습니다 


여러건 조회 (같은 값을 반복해서 조회) 로 넘어갔다 

인덱스가 존재하지 않는 경우에 대해서는 차이가 명확히 드러남

test 3
test 4
test 5

 

내 생각대로라면 caching 까지 더하면 결과가 더 좋게 나와야할 거 같은데 이상하게 소요시간이 더 늘어났다

다른 service 를 호출하고 가져오는 시간에서 더 오래걸리는가 해서 test 3, 4도 service 를 거친 후 값을 가져올 수 있도록 수정했지만 속도가 비슷 -> ai 로 검색해봤다

Cacheable 이 캐시에 데이터가 있는지 확인하고 결과 저장하는 로직이 추가됨
자바 객체를 저장하기 위해 직렬화 하는 과정이 추가됨 -> 인덱스 최적화가 너무 잘 되어있으면 캐시 관리에 비용이 더 많이 드는 경우가 생길 수 있다

 

그래도 캐싱이 어떻게 잘 작동하는지 확인하고 싶어서 cnt 를 100까지 늘려봤다 (조회 횟수 증가시키기)

idx 만: 98 ms
idx + caching : 28 ms

표는 아래와 같다

  기본 (Full scan) idx idx + caching
1번 조회 1084 ms 4 ms (실험 불필요)
5번 조회 5576 ms 8 ms 32 ms
100번 조회 (실험 안 함: 아마 1000ms * 100) 98 ms 28 ms

정리하자면 

 

1) Full scan (기본) 대비 idx 가 약 250~700배 빠른 것을 확인 가능

 

2) caching 의 성능을 확인하기 위해 5번을 조회했을때 (소량 조회 시) 캐싱이 더 느림

-> 캐시 오버헤드 때문: 저장로직, AOP 프록시 거치는 과정에서 손해를 보게됨

 

3) 대량 조회 시 캐시가 인덱스보다 3.5배 빨라짐 

idx 만 사용한 경우 소량(5회) -> 대량(5*20배) 비율을 완전히 따르지 않았지만 12.25배 증가한 시간이 소요되었고

idx 와 캐싱을 함께 사용한 경우 성능이 유지되는 모습을 확인할 수 있었다 


Level 12

EC2, S3, RDS 설정 캡쳐

 

ec2 server inbound rules

22번 포트를 내 ip 만 접속할 수 있도록 수정하면 좋을 것 같다 

 

IAM 설정

 

rds 와 ec2 연결 규칙

s3 버킷 정책
actuator/health api 사용 가능

 

user 가 본인의 image 를 추가할 수 있도록 로직을 추가

@PutMapping("/users/image")
public String updateImage(@AuthenticationPrincipal AuthUser authUser, @RequestParam MultipartFile file){
    return userService.updateImage(authUser.getId(), file);
}
@Transactional
public String updateImage(Long userId, MultipartFile file) {
    User user = userRepository.findById(userId)
            .orElseThrow(() -> new InvalidRequestException("User not found"));

    String imageUrl;

    try{
        String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
        S3Resource resource = s3Template.upload(bucketName, fileName, file.getInputStream());

        imageUrl = resource.getURL().toString();
    }catch (IOException e){
        throw new RuntimeException("file upload failed");
    }

    user.updateProfileImage(imageUrl);
    return user.getUrl();
}

s3 버킷에 이미지가 정상적으로 올라감을 확인

 

'SPARTA 과제 > SPRING' 카테고리의 다른 글

K6 테스트  (0) 2026.04.29
JOOQ 코드 생성 트러블슈팅  (1) 2026.04.13
Plus spring 필수 과제  (0) 2026.04.03
심화) LV 7 Test code  (1) 2026.03.07
심화) LV 6 코드 리팩토링  (0) 2026.03.07