본문 바로가기
dev/프로젝트

[프로젝트] JPA 영속성 Context & AOP

by dev-everyday 2025. 1. 16.
반응형

1. 피드백 반영

feedback > 히스토리 기반으로 데이터를 관리해라.

기본적으로 생성일, 수정일, 수정한 사람, 생성한 사람으로 설정하자.

result > DTO와 Entity들에 createdAt, updatedAt, createdBy, updatedBy 다 추가하였다.

공통적으로 추가해준 부분(순서대로 Dto, Entity)

todo > 생성과 수정은 인증 정보가 있는 사람들에게 부여하도록 변경하기.

private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private String createdBy;
private String updatedBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private String createdBy;
private String updatedBy;

@PrePersist
public void prePersist() {
    this.createdAt = (this.createdAt == null) ? LocalDateTime.now() : this.createdAt;
    this.createdBy = (this.createdBy == null) ? "admin" : this.createdBy;
}

 

feedback > ID는 Long 타입으로 사용하자.

result > ID Integer에서 Long 타입으로 변경.

tip > UUID를 쓸 수도 있고 해시화시킬 수도 있고 다양하게 응용 가능하다.


feedback 
> Entity는 UserEntity가 아니라 User 이런식으로 생성하기.

result > 불필요한 Entity명 제거.

 

feedback > @Transactional은 Insert, Update 등 두 개 이상 있을 경우에 통상적으로 붙이고 class가 아닌 메서드에 붙이자.

result > 서비스 update 부분에 다 붙였다.

todo > JPA 영속성에 대해 공부하기.

tip > methodA와 methodB에 각각 Transactional 붙이고 둘을 호출하는 서비스가 있을 때 @Transactional 설정하면 최상위 Transactional만 반영된다.

 

feedback > package명에 _나 -를 붙여서 표현하지 마라.

result > fortune_app_backend를 fortune.app으로 변경함.


feedback 
> toDto, toEntity Dto와 Entity에 작성하기, 서비스에서 사용하면 서비스마다 선언을 해야해서 각 파일에서 직접 가지고 있는게 낫다.

result > 서비스 toDto 옮기고 mapToEntity 추가함. static으로 추가하고 public으로 선언하였다.

public static CardDto mapToDto(Card entity) {
    return CardDto.builder()
            .cardId(entity.getCardId())
            .name(entity.getName())
            .orientation(entity.getOrientation())
            .build();
}
public static Card mapToEntity(CardDto dto){
    return Card.builder()
            .cardId(dto.getCardId())
            .name(dto.getName())
            .orientation(dto.getOrientation())
            .build();
}
public static CardInterpretation mapToEntity(CardInterpretationDto dto, Card card, Fortune fortune) {
    return CardInterpretation.builder()
            .card(card)
            .fortune(fortune)
            .interpretationContent(dto.getInterpretationContent())
            .createdAt(dto.getCreatedAt() != null ? dto.getCreatedAt() : LocalDateTime.now())
            .updatedAt(dto.getUpdatedAt() != null ? dto.getUpdatedAt() : LocalDateTime.now())
            .createdBy(dto.getCreatedBy() != null ? dto.getCreatedBy() : "admin")
            .updatedBy(dto.getUpdatedBy() != null ? dto.getUpdatedBy() : "admin")
            .build();
}

tip > 하나의 MVC에 여러 DTO가 존재할 수 있다. 그런 경우에 entity에서 한번에 toDTO를 하기도 한다, 꼭 dto가 toDto인 건 아님.

 

feedback > @Getter, @Setter 직접 해보기.

result > 이럴 때 필요한 단축키 Alt+Insert 사용해서 직접 생성하기.

 

feedback > 패키지를 controller, entity, dto 이런식으로 나누지 말고 domain 별로 나누어서 관리하자.

result > 기존 패키지들에서 domain(card, daily, user)로 나누어서 분배함.

but > 도메인 잘 나누었다고 msa는 아니다^^


feedback 
> DTO에 @Setter 다 삭제해라. 주고 받는 용도로 사용하기 때문에 Setter 필요 X.

result > @Setter 안녕..

 

feedback > @Getter, @Setter 직접 해보기.

result > 이럴 때 필요한 단축키 Alt+Insert 사용해서 직접 생성하기.

 

feedback > @Getter, @Setter 직접 해보기.

result > 이럴 때 필요한 단축키 Alt+Insert 사용해서 직접 생성하기. ID와 createdAt, createdBy는 Setter 생성 안함.


feedback 
> @RequiredArgsConstructor 보다 직접 생성자 패턴 써보자. spring 환경에서만 일한다는 보장은 없잖아요? (스쳐가는 과거) framework 의존성을 배제하고 공부해봅시다.

result > 생성자 패턴 해보자. Alt+Insert는 괜찮겠지..?

 

fix > UserDto와 UserEntity 사이를 바꾸다보니 UserCreateRequestDto와 UserResponseDto가 필요할 거 같다는 생각이 들어 추가하였다. 일단은 RequestDto에서는 flat하게 request를 다 준다고 가정하였고 request로는 id값을 전달하도록 변경하였다.

아래는 request body로 UserCreateRequestDto와 response로는 UserResponseDto로 변경하였다.

@MethodTimeCheck
@PostMapping
public UserResponseDto createUser(@RequestBody UserCreateRequestDto dto) {
    return userService.createUser(dto);
}
package com.fortune.app.user;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.Date;

@Getter
@AllArgsConstructor
@Builder
public class UserCreateRequestDto {
    private String provider;
    private String providerUid;
    private String accessToken;
    private String refreshToken;
    private String name;
    private Date birth;
    private String nickname;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private String createdBy;
    private String updatedBy;

    public static UserCreateRequestDto mapToDto(User entity) {
        return UserCreateRequestDto.builder()
                .name(entity.getName())
                .birth(entity.getBirth())
                .nickname(entity.getNickname())
                .createdAt(entity.getCreatedAt())
                .updatedAt(entity.getUpdatedAt())
                .createdBy(entity.getCreatedBy())
                .updatedBy(entity.getUpdatedBy())
                .provider(entity.getOauth().getProvider())
                .providerUid(entity.getOauth().getProviderUid())
                .accessToken(entity.getOauth().getAccessToken())
                .refreshToken(entity.getOauth().getRefreshToken())
                .build();
    }
}
package com.fortune.app.user;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.Date;

@Getter
@AllArgsConstructor
@Builder
public class UserResponseDto {
    private Long userId;
    private Long oauthId;
    private String name;
    private Date birth;
    private String nickname;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private String createdBy;
    private String updatedBy;
    private String provider;
    private String providerUid;
    private String accessToken;
    private String refreshToken;

    public static UserResponseDto mapToDto(User user) {
        return UserResponseDto.builder()
                .userId(user.getUserId())
                .oauthId(user.getOauth().getOauthId())
                .name(user.getName())
                .birth(user.getBirth())
                .nickname(user.getNickname())
                .createdAt(user.getCreatedAt())
                .updatedAt(user.getUpdatedAt())
                .createdBy(user.getCreatedBy())
                .updatedBy(user.getUpdatedBy())
                .provider(user.getOauth().getProvider())
                .providerUid(user.getOauth().getProviderUid())
                .accessToken(user.getOauth().getAccessToken())
                .refreshToken(user.getOauth().getRefreshToken())
                .build();
    }
}
@Transactional
public UserResponseDto createUser(UserCreateRequestDto dto) {
    Oauth oauth = Oauth.builder()
            .provider(dto.getProvider())
            .providerUid(dto.getProviderUid())
            .accessToken(dto.getAccessToken())
            .refreshToken(dto.getRefreshToken())
            .build();
    Oauth savedOAuth = oauthRepository.save(oauth);

    User user = User.builder()
            .name(dto.getName())
            .birth(dto.getBirth())
            .nickname(dto.getNickname())
            .oauth(savedOAuth)
            .build();
    User savedUser = userRepository.save(user);

    return UserResponseDto.mapToDto(savedUser);
}

변경 후 postman 실행 결과

 

docker 다시 실행해서 잘 되는지 확인하였다.

docker start fortune-app-db

2. soft delete & hard delete

soft delete는 isActive와 같은 컬럼을 두고 활성화 여부를 판단한다.

hard delete의 경우에는 cascade all을 통해서 DB에서 지워버리는 delete를 의미한다.

3. 고아 객체 삭제

고아 객체 제거 기능은 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다.

mappedBy = parent, cascade = CascadeType.ALL, orphanRemoval = true

cascade로 놓치는 경우가 있는데 DB에서 수동으로 지울 수도 있지만 고아면 날리는 식으로 처리가 가능하다.

조회 시 비식별 관계여서 누락된 경우 부모가 없으면 데이터를 지워버린다.

cascade = CascadeType.ALL, orphanREmoval = true 이 조합이 hard delete의 세트라고 생각하면 될 듯하고 4번과 함께 공부해보자.

4. JPA 영속성 context

@Transactional
public CardDto updateCard(Long cardId, CardDto dto) {
    Card card = cardRepository.findById(cardId)
            .orElseThrow(() -> new RuntimeException("Card not found"));

    if (dto.getName() != null) card.setName(dto.getName());
    if (dto.getOrientation() != null) card.setOrientation(dto.getOrientation());
    card.setUpdatedAt(LocalDateTime.now());

    Card saved = cardRepository.save(card);
    return CardDto.mapToDto(saved);
}

updateCard에서 repositoroy로 DB에 있는 값을 조회해서 entity에 담았을 때 repository는 entity의 변경 사항을 감지한다.

반환된 Card 엔티티는 JPA의 영속성 컨텍스트에 관리된다고 한다.

이후 setName, setOrientation과 같은 메서드로 엔티티 값을 변경하면 영속성 컨텍스트가 변경 사항을 감지하여 트랜잭션이 끝나는 시점에 자동으로 DB에 반영한다.

public CardDto createCard(CardDto dto) {
    Card card = Card.builder()
            .name(dto.getName())
            .orientation(dto.getOrientation())
            .build();
    Card saved = cardRepository.save(card);
    return CardDto.mapToDto(saved);
}

그렇다면 createCard도 영속성 컨텍스트에 관리될까?

아니다, saved는 영속성이지만 직접 create하는 경우에는 영속성이 없다.

따라서 Transactional 역시 필요하지 않다.

 

그렇다면 JPA 영속성이 뭐냐고 물어봤을 때 뭐라고 대답하는 게 좋을까?

JPA 영속성 컨텍스트는 엔티티 객체를 관리하는 논리적 저장소로 엔티티가 영속성 컨텍스트에 들어오면 JPA가 해당 객체를 관리하면서 상태 변화를 추적한다.

직접 SQL을 작성하지 않아도 변경된 데이터를 DB에 반영할 수 있다.

 

JPA 영속성 관련 정리

a) 영속성 컨텍스트

엔티티 객체를 1차 캐시에 저장하고 관리하는 공간으로 EntityManager가 이를 관리하며 트랜잭션 단위로 유지된다.

b) 엔티티 생명 주기

비영속(Transient) : JPA와 관련 없고 new로 생성된 객체

영속(Managed) : 영속성 컨텍스트에서 관리되는 상태로 find, save를 통해 호출하는 경우가 해당함

준영속(Detached) : 영속성 컨텍스트에서 더 이상 관리되지 않는 상태로 detach, clear를 호출한 상황임

삭제(Removed) : 삭제 대기 상태로 remove 호출한 상황임

c) 변경 감지(Dirty Checking)

영속성 컨텍스트에서 관리 중인 엔티티가 변경되면 트랜잭션 종료 시점에 변경 사항을 감지하여 자동으로 DB에 반영한다.

이를 위해 영속성 컨텍스트는 엔티티의 스냅샷을 저장하고 변경된 값과 비교한다.

d) 플러시(Flush)

영속성 컨텍스트의 변경 내용을 DB에 반영하는 과정으로 트랜잭션이 커밋되기 전에 수행한다.

5. AOP (Aspect Oriented Programming)

메서드 실행 시간을 계산하는 custom annotation이 만들고 싶다고 가정하자.

common 패키지와 aop 패키지를 생성한다.
먼저 common 패키지에 MethodTimeCheck 인터페이스를 만든다.

package com.fortune.app.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodTimeCheck {

}

@interface @Target 은 Method로 설정한다.

 

aspect 패키지에는 MethodTimeCheckAspect를 만든다.

@Aspect는 이 클래스가 AOP 역할을 하는 Aspect임을 나타낸다.

package com.fortune.app.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MethodTimeCheckAspect {

    @Around("@annotation(MethodTimeCheck)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().toShortString();

        System.out.println(methodName + " 실행 시간: " + (end - start) + "ms");
        return result;
    }
}

 

파라미터로 joinPoint를 받는데 joinPoint는 메서드를 실행하는 순간이고 return result를 해야 정상적으로 메서드가 실행된다.

Around는 실행 시점을 설정하는데 마음대로 조절할 수 있고 MethodTimeCheck annotation 기반으로 동작하도록 설정하였다.

물론 @Before, @After, @AfterReturning, @AfterThrowing도 있다.

패키지 전체를 설정할 수도 있고 annotation을 범주로 설정할 수 있고 커스터마이징이 가능하다.

AfterReturning은 메서드가 잘 실행되고 끝난 후 실행하고 AfterThrowing은 메서드 실행에 Exception이 발생했을 때 실행한다.

pointcut이라는 것도 있는데 public void customPoint(){} 로 설정하고 @Pointcut("execution()")으로 설정할 수 있다.

이 경우에는 execution 내부에 ~service..*(..) 이런식으로 설정 시 해당 서비스 하위에 다 적용된다.

Pointcut은 C++의 define 같은 느낌으로 미리 선언하고 이후에 @Before("customPoint()") 이런 식으로 호출이 가능하다.

나중에 직접 하나하나 작성하는 것보다 미리 작성한 후 가져다 쓰는 편리함이 있다.

이후에 @MethodTimeCheck를 메서드에 붙여서 실행하면 AOP가 해당 메서드만 가로채 실행 시간 측정 로직을 추가하면 된다.

 

AOP 주요 개념

a) AOP란?

AOP는 소프트웨어 개발에서 공통으로 사용하는 부가적인 로직을 핵심 비즈니스 로직과 분리해서 모듈화하는 프로그래밍 패턴이다.

b) Aspect

부가적인 기능을 정의한 모듈로 애플리케이션의 관점을 나타낸다.

c) JoinPoint

애플리케이션 실행 중 AOP 코드가 삽입될 수 있는 시점으로 메서드 호출, 생성자 호출, 필드 접근 등이 있다.

d) Pointcut

어떤 JoinPoint에 AOP 로직을 적용할지를 정의한다.

e) Advice

JoinPoint에서 실행될 코드로 Before, After, Around, AfterReturning, AfterThrowing 등이 있다.

f) Weaving

AOP 기능을 애플리케이션 코드에 적용하는 과정으로 컴파일 시, 클래스 로딩 시, 런타임 시 적용이 가능하다.

g) AOP를 사용하는 이유

AOP를 사용하면 여러 로직을 한 곳에 모아서 관리가 가능하고 핵심 로직과 부가 로직을 분리할 수 있다.

또한 코드 재사용성이 증가하고 관심사가 분리되어 핵심 기능과 공통 관심사 분리를 통해 각 기능에만 집중이 가능하다.

h) AOP 사용 예시

로깅이나 트랜잭션 관리, 보안, 성능 모니터링, 캐싱 등이 있다.

@Transactional은 Spring AOP를 활용한 예로 트랜잭션 시작, 커밋, 롤백 로직을 비즈니스 로직에서 분리해서 처리한다.

 

6. SpringSecurity

SpringSecurity는 스프링 기반 애플리케이션의 보안을 제공하는 프레임워크이다.

인증과 권한 부여를 중심으로 보안 기능을 제공한다.

주요 기능은 인증(Authentication)과 권한 부여(Authorization), 그리고 보안 정책 관리(CORS, CSRF)이다.

SpringSecurity 설정은 SecurityConfig 파일에 설정해야한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()  
                .anyRequest().authenticated()         
            .and()
            .formLogin();                             
    }
}

 

a) CORS(Cross-Origin Resource Sharing)

CORS는 다른 출처 간에 리소스를 공유할 수 있도록 허용하는 보안 메커니즘이다.

브라우저의 기본 보안 정책인 SOP(Same-Origin Policy)를 우회하기 위한 방법이다.

동작 원리는 브라우저는 HTTP 요청 전 서버에 이 request를 보내도 되는지 사전 확인을 요청(Preflight Request)하고 서버는 적절한 헤더로 응답한다. 이후 브라우저가 서버 응답을 기반으로 요청을 보낼지를 결정한다.

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("https://example.com") // 허용할 도메인
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}

출처를 모르는 다른 도메인이 공격하면 이를 막기 위해 CORS 에러가 발생한다.

 

b) A Record(Address Record)

A 레코드는 DNS에서 도메인 이름을 IPv4 주소로 매핑하는 기록이다.

사용자가 도메인 이름을 입력하면 A 레코드를 통해 해당 도메인에 연결된 서버의 IP 주소를 반환한다.

도메인 example.com은 IP 주소 192.0.2.1로 매핑된다.

example.com.    IN    A    192.0.2.1

 

c) CNAME Record(Canonical Name Record)

CNAME 레코드는 DNS에서 별칭을 설정하는 데 사용된다.

하나의 도메인을 다른 도메인으로 매핑하여 최종적으로 연결된 A 레코드 또는 IP 주소를 참조한다.

정리하자면, A 레코드는 도메인 이름과 직접적인 IP 주소를 매핑하면 CNAME 레코드는 도메인 이름을 다른 도메인 이름으로 매핑한다.

www.example.com.    IN    CNAME    example.com.

www.example.com 요청은 example.com을 참조하며 example.com의 A 레코드를 통해 최종 IP 주소로 연결된다.

 

d) CSRF(Cross-Site Request Forgery)

CSRF는 웹 보약 취약점 중 하나로 사용자가 신뢰하는 웹 애플리케이션에 대해 악의적인 요청을 보내는 공격이다.

사용자가 인증된 세션을 유지하고 있을 때 공격자가 해당 사용자의 권한을 악용해서 서버에 원하지 않는 동작을 수행하도록 한다.

CSRF 방지를 위해 CSRF 토큰을 사용해서 유효한 요청인지 확인하는 방법이 있다.

CSRF 토큰은 쿠키나 세션과 연동되어 공격자가 예측하기 어렵다.

Referer Header 검증을 통해서 요청의 Referer 또는 Origin 헤더를 확인해 신뢰할 수 있는 출처의 요청인지 검증할 수도 있다.

CORS 정책을 강화해서 서버에서 허용된 출처에서만 요청을 받을 수 있도록 제한할 수도 있다.

Spring Security는 기본적으로 CSRF 방어 기능을 활성화한다.

 

e) FormLogin

FormLogin은 Spring Security에서 제공하는 인증 방식 중 하나로 사용자가 웹 애플리케이션의 로그인 페이지를 통해 사용자 이름과 비밀번호를 입력해 인증을 수행하는 방식이다.

기본적으로 HTTP 폼을 통해 인증 정보를 서버로 전송하고 서버는 이를 검증하고 세션을 통해 사용자를 인증 상태로 유지한다.

Spring Security는 기본적으로 /login URL을 제공하고 사용자가 브라우저에서 접근할 수 있는 로그인 폼을 렌더링한다.

다만 API 중심의 애플리케이션에서는, RESTful API 기반일 경우에는 JWT 또는 OAuth2와 같은 토큰 기반 인증 방식이 더 적합하다.

/login은 @Controller를 사용하는데 @RestController를 사용하면 Body에 data를 직렬화해서 보내므로 페이지를 return할 수 없기 때문에 Controller를 사용한다.

resouce/web_inf/login.jsp가 있다 할 때 return ("login")하면 로그인 페이지가 리턴된다.

7. MVC 구조

MVC 구조는 Model-View-Controller의 약자이며 소프트웨어 디자인 패턴이다.

어플리케이션을 세 가지 주요 역할로 분리해 개발의 효율성과 유지보수성을 높인다.

a) Model

애플리케이션의 데이터 및 비즈니스 로직을 담당하고 데이터베이스와의 상호작용, 데이터를 처리하거나 상태를 관리하는 역할이다.

엔티티 클래스가 이에 해당한다.

b) View

UI 및 데이터를 사용자에게 표시하는 역할로 모델 데이터를 기반으로 사용자에게 결과를 렌더링한다.

HTML, JSP, Thymeleaf 템플릿 등이 있다.

c) Controller

사용자의 입력을 처리하고 요청을 적절한 서비스 또는 모델로 전달한다.

모델에서 데이터를 가져와 뷰를 전달하는 역할을 한다.

@Controller 또는 @RestController가 이에 해당한다.

@Controller는 모델 객체를 만들어서 데이터를 담고 뷰를 전달하지만 @RestController는 단순히 객체만을 반환하고 객체 데이터는 JSON 또는 XML 형식으로 HTTP 응답에 담아 전송한다.

더보기

1. MVC 구조의 장점은 뭔가요?

MVC 구조의 장점은 애플리케이션의 관심사를 분리하여 유지보수성과 확장성을 높일 수 있습니다.

 

2. MVC에서 Controller 역할은 뭐에요?

Controller는 사용자의 요청을 처리하고 요청에 필요한 데이터를 Model에서 가져와 View에 전달합니다.

Model과 View의 중간 연결 역할을 수행합니다.

 

3. Spring MVC의 DispatcherServlet의 역할은 뭐에요?

DispatcherServlet은 Spring MVC의 중심으로 클라이언트 요청을 받아 적절한 Controller로 전달하고 처리 결과를 ViewResolver를 통해 View로 렌더링합니다.

 

4. MVC 단점이 뭐에요?

프로젝트가 복잡해질 수록 Controller가 무거워질 수 있고 Model과 View의 데이터 바인딩 과정에서 의존성이 발생할 수 있습니다.

8. N+1 문제 다시 정리

N+1 문제는 ORM에서 발생하는 성능 문제로 한 번의 쿼리로 처리할 수 있는 작업을 여러 번의 쿼리로 처리하면서 불필요한 DB의 호출이 증가하는 현상이다.

해결 방법으로는 Fetch Join을 사용해서 한 번의 쿼리로 가져오는 방법과 EntityGraph를 사용해서 특정 엔티티 그래프를 정의해서 데이터를 가져오는 방법이 있고 Batch Fetching을 통해서 정해진 크기의 데이터만 가져오는 방법이 있습니다.

더보기

1. N+1 문제가 뭐에요?

N+1문제는 ORM에서 발생하는 성능 문제로 한 번의 쿼리로 처리할 수 있는 작업을 추가적으로 N개의 쿼리를 통해 처리하게 되는 현상입니다. 이로 인해 DB 호출 횟수가 불필요하게 증가합니다.

 

2. 어떻게 해결해봤어요?

Fetch Join을 사용해서 연관된 엔티티를 한 번의 쿼리로 가져오기도 하였고 실제로 서비스 기획에 따라서 한 번에 가져오는 경우가 유리하면 Fetch Join으로 가져오게 하였고 이후에 가져오는 게 좋은 경우에는 Lazy Join으로 가져오게 구현하였습니다.

9. 람다와 스트림 연습

Lambda는 자바에서 함수형 프로그래밍을 지원하기 위해 도입된 기능으로 익명 함수를 간결하게 작성할 수 있다.

Stream은 자바 컬렉션을 처리하고 변환하기 위한 API로 데이터 필터링, 매핑, 집계 등의 작업을 간결하게 수행할 수 있다.

중간 연산(filter, map)과 최종 연산(forEach, collect)로 구분된다.

 

a) Lambda

// 단일 매개변수
x -> x * x
// 여러 매개변수
(x, y) -> x + y
// 코드 블록
(x, y) -> {
    int sum = x + y;
    return sum * 2;
}

Java에서 제공하는 Functional Interface로 연습해보자.

Consumer<T> 값을 소비하고 반환값이 없다.

Function<T, R> 입력을 받아서 다른 타입의 결과 반환한다.

Predicate<T> 조건 테스트 Boolean을 반홚나다.

Supplier<T> 값을 생성한다.

Consumer<String> print = s -> System.out.println(s);
print.accept("Hello, Lambda!");

Function<Integer, String> intToString = x -> "Number: " + x;
System.out.println(intToString.apply(5));

Predicate<Integer> isEven = x -> x % 2 == 0;
System.out.println(isEven.test(4)); // true

 

b) Stream

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

names.stream()
    .filter(name -> name.startsWith("A"))
    .forEach(System.out::println);

names.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);

filter는 조건에 맞는 요소만 남기고 map은 각 요소를 변환하고 sorted는 정렬 collect는 결과를 리스트나 맵으로 변환한다.

10. TODO

- Docker Redis 이미지 다운 후 Spring Boot와 연결 & 실행

- JPARepository에 JPQL 활용해서 작성해보기

반응형