강의 : 스프링부트로 대규모 시스템 설계 - 게시판

대규모 시스템으로 설계된 게시판에 사용된 Spring 문법과 요소 기술 - Java

1. LocalDateTime/Duration

1-1. LocalDateTime

1-1-1. LocalDateTime.now()

현재 로컬 컴퓨터의 날짜와 시간을 반환

// 2025-02-23T11:58:20.551705
LocalDateTime.now();

1-1-2. 비교

  • isAfter(LocalDateTime): 인자보다 미래 시간이면 true 반환
  • isBefore(LocalDateTime): 인자보다 과거 시간이면 true 반환
  • isEqual(LocalDateTime): 인자와 같은 시간이면 true 반환
  • compareTo(LocalDateTime)
    • 0: 인자보다 미래 시간

    • < 0: 인자보다 과거 시간
    • == 0: 인자와 같은 시간

1-1-3. ofInstant(Instant, Zone)

java.time.Instant는 1740372254736과 같이 시간을 정수로 표기한 정보를 가진다.
Date에서 LocalDateTime으로 바로 전환이 불가능 하므로 아래 코드와 같이 Instant를 활용해 변환한다.
Instant.toEpochMilli() 의 반환 값인 epoch초는 1970년 1월 1일 표준 자바 epoch 시간 부터 측정된 값이다.

Date date = new Date();
LocalDateTime localDateTime = Instant.ofInstant(
    Instant.ofEpochMilli(date.getTime()),
    ZoneId.systemDefault()
);

1-2. Duration

시간 간격을 초와 나노초로 표현한다. 간격을 계산하는데 시, 분을 사용할 수 있다.
시간 간격은 long 타입의 최대값 만큼 저장할 수 있다.

자주 사용하는 함수

  • Duration.ofSeconds(long seconds): 인자로 받은 크기 만큼의 초를 표현한다.
  • Duration.plusSeconds(long secondsToAdd): 인자로 받은 크기 만큼의 초를 더한다.
  • Duration.ofDays(long days): 인자로 받은 크기 만큼의 날을 표현한다. 하루는 24시간으로 계산한다.
  • Duration.plusDays(long dayToAdd): 인자로 받은 크기 만큼의 날을 더한다. dayToAdd * 86400 한 값을 더한다.
  • Duration.between(Temporal startInclusive, Temporal endExclusive): 시작 시간(startInclusive)과 끝 시간(endExclusive) 사이의 간격을 계산한다. Temporal은 LocalDateTime, Instant 객체를 주로 사용한다.

2. CountDownLatch

다른 스레드에서 동작 중인 작업들이 끝날 때 까지 하나 이상의 스레드가 기다리도록 해주는 동기화 도구이다.
CountDownLatch는 세려는 값을 인자로 받아 초기화 된다.
await() 함수는 countDown() 함수를 통해 현재 카운트가 0이 될 때 까지 blocking 하고 0이 되면 즉시 리턴한다.
CounDownLatch의 카운트 값은 다시 초기화 될 수 없다.
카운트를 초기화 해 여러 번 수행이 필요한 경우 CountDownLatch 대신 CyclicBarrier를 고려한다.


아래 코드는 N개의 작업자 스레드가 startSignal이 0이 될 때 까지 기다린 후 작업을 완료할 때 까지 main 함수가 동작하는 스레드가 기다리는 예제이다.

class Driver { // ...
    void main() throws InterruptedException {
        CountDownLatch startSignal = new CountDownLatch(1);
        CountDownLatch doneSignal = new CountDownLatch(N);

    for (int i = 0; i < N; ++i) // create and start threads
        new Thread(new Worker(startSignal, doneSignal)).start();

        doSomethingElse();            // don't let run yet
        startSignal.countDown();      // let all threads proceed
        doSomethingElse();
        doneSignal.await();           // wait for all to finish
    }
}

class Worker implements Runnable {
    private final CountDownLatch startSignal;
    private final CountDownLatch doneSignal;
    Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
        this.startSignal = startSignal;
        this.doneSignal = doneSignal;
    }
    public void run() {
        try {
            startSignal.await();
            doWork();
            doneSignal.countDown();
        } catch (InterruptedException ex) {} // return;
    }

    void doWork() { ... }
 }

3. Optional<T>

코드 작성 중 null 값을 가지는 경우를 처리하기 위해 번거로운 조건문 코드를 작성하는 일이 종종 생긴다.
그리고 NullPointExeption이 발생해 기능이 정상적으로 동작하지 않는 경우가 종종 발생한다.
Java 8 이후로 null을 가질 수 있는 값을 감싸는 wrapper 클래스인 Optional<T>을 지원해 이런 문제들을 해결한다.

  • empty() : 빈 값을 가지는 Optional 인스턴스를 반환한다.
    Optional.empty()
    
  • equals(Object obj) : obj를 갖는지 확인한다.

  • filter(Predicate<? super T> predicate) : 값이 존재하고 주어진 Predicate와 일치하면 값을 갖는 Optional을 반환하고 아니면 비어있는 Optional을 반환한다.
commentRepository.indById(parentCommentId).filter(not(Comment::getDeleted))
  • get() : Optional에 값이 있으면 값을 리턴하고 아니면 NoSuchElementException 예외를 던진다.

  • ifPresent(Consumer<? super T> consumer): 값이 존재하면 전달된 Consumer를 실행하고 없으면 아무 일도 하지 않는다.

commentRepository.findById(commentId)
                .filter(not(Comment::getDeleted))
                .ifPresent(comment -> {
                    if (hasChildren(comment)) {
                        comment.delete();
                    } else {
                        delete(comment);
                    }
                })
  • isPresent() : 값이 있으면 true를 반환하고 없으면 false를 반환한다.

  • map(Function<? super T,? extends U> mapper) : 값이 있으면 제공된 매핑 함수를 해당 값에 적용하고, 결과가 null이 아니면 결과를 설명하는 Optional을 반환한다.

@Getter
@ToString
public class ArticleLikeResponse {
    ...

    public static ArticleLikeResponse from(ArticleLike articleLike) {
        ArticleLikeResponse response = new ArticleLikeResponse();
        ...
        return response;
    }
}

articleLikeRepository.findByArticleIdAndUserId(articleId, userId)
                .map(ArticleLikeResponse::from)
                .orElseThrow();
  • of(T value) : null이 아닌 값을 갖는 Optional을 반환한다

  • ofNullable(T value) : null이 아니면 지정된 값을 갖는 Optional을 반환하고, 그렇지 않으면 빈 Optional을 반환한다.

Optional.ofNullable(articleResponse);
  • orElse(T other) : 값이 있으면 반환하고 그렇지 않으면 other를 반환한다.
articleLikeCountRepository.findById(articleId)
                .map(ArticleLikeCount::getLikeCount)
                .orElse(0L);
  • orElseGet(Supplier<? extends T> other) : 값이 있으면 반환하고 그렇지 않으면 Supplier other의 결과를 반환한다.
ArticleLikeCount articleLikeCount = articleLikeCountRepository.findLockedByArticleId(articleId)
                .orElseGet(() -> ArticleLikeCount.init(articleId, 0L));
  • orElseThrow(Supplier<? extends X> exceptionSupplier) : 값이 있으면 반환하고 없으면 예외를 던진다.
ArticleResponse.from(articleRepository.findById(articleId).orElseThrow());

4. Predicate

인자를 받아 boolean 값을 반환하는 함수형 인터페이스이다.

  • test(T t) : 주어진 인자를 검증한다
  • and(Predicate<? super T> other) : 다른 Predicate와 AND 조건으로 연결한다.
  • or(Predicate<? super T> other) : 다른 Predicate와 OR 조건으로 연결한다.
  • Predicate not(Predicate<? super T> target) :
private boolean hasChildren(Comment comment) {
        return commentRepository.countBy(comment.getArticleId(), comment.getCommentId(), 2L) == 2;
    }

public void delete(Comment comment) {
    commentRepository.delete(comment);
    if (!comment.isRoot()) {
        commentRepository.findById(comment.getParentCommentId())
                .filter(Comment::getDeleted)
                .filter(Predicate.not(this::hasChildren))
                .ifPresent(this::delete);
    }
}