반응형

Spring Security는 Spring 기반 애플리케이션의 보안을 제공하는 강력하고, 유연한 프레임워크입니다.

특징

 

인증(Authentication)과 인가(Authorization) 기능 제공

다양한 보안 공격 방어 (CSRF, XSS 등)

확장 가능하고 커스터마이징이 용이

 

 

프로젝트 라이브러리

build.gradle

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'

spring security 뿐 아니라 API 에서 jwt 토큰을 인증 및 인가 하는 기능을 사용하려면 jsonwebtoken 라이브러리도 추가하여야 한다. 

 

인증 개념

(Authentication)

인증은 사용자의 신원을 확인하는 과정입니다.

 

인가 개념

(Authorization)

인가는 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다.

 

JwtRequestFilter

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    private UserDetailsService userDetailsService;
    private JwtUtil jwtUtil;
    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Autowired
    public void setJwtUtil(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {

                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

 

JwtUtil

@Component
public class JwtUtil {

//    private String SECRET_KEY = Base64.getEncoder().encodeToString("secret".getBytes());

    private final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);


    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails) {
        return createToken(userDetails.getUsername());
    }

    private String createToken(String subject) {
        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간 유효
                .signWith(SECRET_KEY)
                .compact();
    }


    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

 

SecurityConfig

@Configuration
public class SecurityConfig {

    private final JwtRequestFilter jwtRequestFilter;

    //@Lazy 이렇게 하면 JwtRequestFilter 빈의 초기화를 지연시켜 순환 참조 문제를 피할 수 있습니다.
    @Autowired
    public SecurityConfig(@Lazy JwtRequestFilter jwtRequestFilter) {
        this.jwtRequestFilter = jwtRequestFilter;
    }



    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//        http
//                .authorizeHttpRequests(authorizeRequests ->
//                        authorizeRequests
//                                .requestMatchers("/admin/**").hasRole("ADMIN")
//                                .requestMatchers("/user/**").hasRole("USER")
//                                .anyRequest().authenticated()
//                )
//          •  authorizeHttpRequests 메서드를 사용하여 HTTP 요청에 대한 인가(Authorization) 규칙을 설정합니다.
//          •  requestMatchers("/admin/**").hasRole("ADMIN"): URL 패턴이 /admin/**인 요청은 ADMIN 역할을 가진 사용자만 접근할 수 있도록 설정합니다.
//          •   requestMatchers("/user/**").hasRole("USER"): URL 패턴이 /user/**인 요청은 USER 역할을 가진 사용자만 접근할 수 있도록 설정합니다.
//          •   anyRequest().authenticated(): 나머지 모든 요청은 인증된 사용자만 접근할 수 있도록 설정합니다.

//        .formLogin(formLogin ->
//                formLogin
//                        .loginPage("/login")
//                        .permitAll()
//        )
//          •  formLogin 메서드를 사용하여 폼 기반 로그인을 설정합니다.
//          •   loginPage("/login"): 사용자 정의 로그인 페이지를 설정합니다. 사용자가 /login URL로 접근하면 로그인 페이지가 표시됩니다.
//          •   permitAll(): 로그인 페이지는 인증되지 않은 사용자도 접근할 수 있도록 허용합니다.
//
//                .csrf(csrf -> csrf.disable()); // CSRF 보호 비활성화. 필요한 경우 활성화 가능
//          •  csrf 메서드를 사용하여 CSRF(Cross-Site Request Forgery) 보호 설정을 합니다.
//          •  csrf.disable(): CSRF 보호를 비활성화합니다. CSRF 보호가 필요할 경우 이 부분을 제거하거나 다른 방법으로 설정할 수 있습니다.
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/api/auth/**").permitAll()
                                .anyRequest().authenticated()
                )
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsManager = new InMemoryUserDetailsManager();

        var user = User.withUsername("user")
                .password(passwordEncoder().encode("password"))
                .roles("USER")
                .build();

        var admin = User.withUsername("admin")
                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN")
                .build();

        userDetailsManager.createUser(user);
        userDetailsManager.createUser(admin);

        return userDetailsManager;
    }

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


    /**
     * Spring Security 설정 파일에서 AuthenticationManager를 빈으로 정의해야 합니다.
     * Spring Boot 2.x에서는 AuthenticationManager를 직접 정의하는 것이 필요했지만,
     * Spring Boot 3.x에서는 이를 자동으로 구성하지 않으므로 명시적으로 설정해야 합니다.
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

 

user 와 password 라는 아이디 및 비밀번호를 이용하면 로그인 가능하다.

 

AuthController

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
        );

        final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails);

        return jwt;
    }
}

 

AuthRequest

@Getter
@Setter
public class AuthRequest {
    private String username;
    private String password;
}

 

 

로그인시 

curl --location 'http://localhost:8080/api/auth/login' \
--header 'Content-Type: application/json' \
--data '{
"username": "user",
"password": "password"
}'

이렇게 요청할 경우 JWT 토큰을 응답 받고 응답받은 토큰을 API Request 헤더에 
Authtication : Bearar {토큰} 
으로 넣어주면 인가를 해주면 됩니다. 

인가를 원하지 않으면 SecurityConfig 클래스의 securityFilterChain 에 permit 하는 경로를 추가해 주면 됩니다.

 

Spring Security 5.4 이후부터는 WebSecurityConfigurerAdapter 대신 SecurityFilterChain을 사용하여 보안 구성을 합니다.

 

 

Spring Security에서 인증 및 인가 구현 시 사용되는 주요 개념들

 

1. Filter 체인

설명: Spring Security는 다양한 필터들의 체인으로 구성됩니다. 각 필터는 Request를 가로챈 후 일련의 절차를 처리합니다.

주요 필터: UsernamePasswordAuthenticationFilter는 사용자가 제출한 인증 정보를 처리합니다.

2. UsernamePasswordAuthenticationToken 생성

설명: UsernamePasswordAuthenticationFilter는 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에게 전달합니다. 이 토큰에는 사용자가 제출한 인증 정보가 포함되어 있습니다.

3. AuthenticationManager

설명: AuthenticationManager는 실제로 인증을 수행하는데, 여러 AuthenticationProvider들을 이용합니다.

4. AuthenticationProvider

설명: 각각의 Provider들은 특정 유형의 인증을 처리합니다. 예시로 DaoAuthenticationProvider는 사용자 정보를 데이터베이스에서 가져와 인증을 수행합니다.

5. PasswordEncoder

설명: 인증과 인가에서 사용될 패스워드의 인코딩 방식을 지정합니다.

6. UserDetailsService

설명: AuthenticationProvider는 UserDetailsService를 사용하여 사용자 정보를 가져옵니다. UserDetailsService는 사용자의 아이디를 받아 loadUserByUsername을 호출하여 해당 사용자의 UserDetails를 반환합니다.

7. UserDetails

설명: UserDetails에는 사용자의 아이디, 비밀번호, 권한 등이 포함되어 있습니다.

8. Authentication 객체 생성

설명: 인증이 성공하면, AuthenticationProvider는 Authentication 객체를 생성하여 AuthenticationManager에게 반환합니다. 이 Authentication 객체에는 사용자의 세부 정보와 권한이 포함되어 있습니다.

9. SecurityContextHolder

설명: 현재 실행 중인 스레드에 대한 SecurityContext를 제공합니다.

10. SecurityContext

설명: 현재 사용자의 Authentication이 저장되어 있습니다. 애플리케이션은 SecurityContextHolder를 통해 현재 사용자의 권한을 확인하고, 인가 결정을 합니다.

 

인증 및 인가 설정에 대한 주요 사항

 

1. CSRF 보호 비활성화

설명: CSRF 토큰을 사용하지 않으므로 확인하지 않도록 설정합니다.

2. CORS 설정 적용

설명: 다른 도메인의 웹 페이지에서 리소스에 접근할 수 있도록 허용합니다.

3. 폼 로그인과 HTTP 기본 인증 비활성화

설명: Spring 웹 페이지에서 제공되는 로그인 폼을 통해 사용자를 인증하는 메커니즘과 HTTP 기본 인증을 비활성화합니다.

4. JwtAuthFilter 추가

설명: UsernamePasswordAuthenticationFilter 앞에 JwtAuthFilter를 추가하여, JWT 필터를 거치도록 설정합니다. JwtAuthFilter를 통해서 Authentication을 획득하였다면 인증된 자원에 접근할 수 있습니다.

5. 권한에 대한 규칙 작성

설명: anyRequest().permitAll() 설정은 기본적으로 모두 허용해 주지만, EnableGlobalMethodSecurity를 설정하는 이유는 Annotation으로 접근 제한을 설정하기 위함입니다.

추가 설명: 만약 Annotation을 통해 접근 제한을 하지 않을 것이라면, anyRequest() 부분에서 접근 승인할 엔드포인트들을 작성해야 합니다.

6. 인증과 인가 실패 시 Exception Handler 추가

설명: Security 단계에서 권한 관련 401이나 403 에러 등을 처리해 줄 핸들러를 함께 등록해줍니다.

authenticationEntryPoint: 인증되지 않은 사용자에 대해 처리하는 핸들러 정의

accessDeniedHandler: 인증되었지만, 특정 리소스에 대한 권한이 없는 경우(인가) 처리하는 핸들러 정의

 

요약

CSRF 보호 비활성화: CSRF 토큰 사용하지 않음.

CORS 설정: 다른 도메인의 웹 페이지에서 리소스 접근 허용.

폼 로그인과 HTTP 기본 인증 비활성화: Spring 웹 페이지의 로그인 폼과 HTTP 기본 인증 비활성화.

JwtAuthFilter 추가: UsernamePasswordAuthenticationFilter 앞에 JWT 필터 추가.

권한 규칙 작성: anyRequest().permitAll() 설정과 EnableGlobalMethodSecurity 설정.

Exception Handler 추가: 인증과 인가 실패 시 처리할 핸들러 정의.

반응형
반응형

우선 이것을 하기 위해서는 앞에 선행 작업들이 이루어져야 합니다.

 

  1. 인스턴스를 생성하고 간단한 웹서비스 실행
  2. 도메인 구매 사이트 & 네임서버 변경

 

외부 도메인에 네임서버를 GCP로 변경

GCP에 접속한뒤 네트워크 서비스 > Cloud DNS 메뉴에 들어간 뒤, 영역만들기로 하나 생성합니다.

원래는 domain 을 구매한 서비스에 ip 만 등록하면 되나 gcp 의 경우 gcp 내부에 cloud dns 에 도메인을 추가로 등록해줘야 dns 가 동작을 합니다.

 

네트워크 서비스 > cloud DNS > 영역 만들기 

상단에 영역 만들기 통해서 만들어 줍니다. 

여기 까지 생성하게 되면 dns 목록에 뜨게 됩니다. 

이름만 설정하고 나머지 옵션은 기본으로 두시고 사용하셔도 됩니다. 

기본으로 만드시면 SOA 와 NS 가 기본으로 생성됩니다. 

 

NS 목록을 펼치기를 하시면 도메인 구매한 사이트에서 네임서버를 설정할 수 있어 아래 값들을 네임서버 구매하신곳에 등록해주셔도 되고, 등록을 안해주셔도 사용은 가능합니다.

등록을 하게 되면 도메인 서버가 cloud dns 의 설정을 따른다는 이야기 입니다. 

ip 로만 리다이렉트 시킬 용도면 등록하지 않으셔도 동작은 가능합니다. 

 

vm 인스턴스의 외부 ip 를 도메인을 구매한 구매처에 이제 등록 하시고

저는 네입칩 namecheap 을 사용했습니다. 

이 작업을 cloud dns 에도 적용하면 됩니다. 

cloud dns 에서 표준추가 로 dns 설정을 추가합니다. 

subdomain 을 요청하시면 앞에 추가 하면 됩니다.

A 레코드는 ip 주소로 리다이렉트 하는 유형입니다. 

 

gcp 를 사용하실때는 위의 작업까지 해야 dns 에 등록된 gcp Ip 서버로 요청이 됩니다.

 

반응형
반응형

Compute Engine 인스턴스 생성

1. GCP 콘솔에서 Compute Engine > VM 인스턴스로 이동합니다.

2. 인스턴스 만들기를 클릭합니다.

3. 인스턴스 설정:

이름: 원하는 이름 입력

지역: 가까운 지역 선택

머신 유형: e2 선택

부팅 디스크: Ubuntu (최신 LTS 버전) 선택

4. 네트워킹, 디스크, 보안 설정을 필요에 따라 구성합니다.

5. 만들기를 클릭하여 인스턴스를 생성합니다.

 

3. SSH를 통해 인스턴스에 접속

인스턴스가 생성되면 브라우저 창에서 열기를 통해 터미널 열 수 있다.

 

4. Docker 설치

SSH 세션에서 다음 명령어를 실행하여 Docker를 설치합니다:

sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install docker-ce
sudo usermod -aG docker ${USER}

위 명령어를 한줄씩 실행하여 docker 를 설치합니다. 

 

5. Docker Compose 설치

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

 

6. Docker Compose 파일 작성

mkdir wordpress-docker
cd wordpress-docker
nano docker-compose.yml

 

다음 내용을 docker-compose.yml 파일에 입력합니다:

version: '3.1'

services:
  wordpress:
    image: wordpress
    restart: always
    ports:
      - "80:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wp-vol:/var/www/html
  db:
    image: mysql:5.7
    restart: always
    volumes:
      - mysql-vol:/var/lib/mysql
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_ROOT_PASSWORD: rootpass
volumes:
    mysql-vol: {}
    wp-vol: {}

 

7. Docker Compose 실행

sudo docker-compose up -d

 

8. 워드프레스 설정

 

웹 브라우저를 열고 http://YOUR_INSTANCE_IP에 접속하여 워드프레스 설정을 완료합니다.

데이터베이스 정보는 docker-compose.yml 파일에서 설정한 대로 입력합니다.

 

이제 GCP에서 Docker를 사용하여 워드프레스를 성공적으로 설치하였습니다.

 

생성된 인스턴스의 외부 ip 를 브라우져에 입력하면 아래와 같이 워드프레스 초기 설정이 뜹니다.

반응형
반응형

비동기 작업에서 Executor를 사용하는 방법은 CompletableFuture의 비동기 작업을 특정 Executor를 통해 실행하도록 설정하는 것입니다. 기본적으로 CompletableFuture.supplyAsyncCompletableFuture.runAsync는 공용 ForkJoinPool의 공용 스레드 풀을 사용하지만, 특정 Executor를 지정하여 사용자 정의 스레드 풀을 사용할 수도 있습니다.

 

Executor를 사용하는 방법

 

1. Executor 생성

 

먼저, 사용할 Executor를 생성해야 합니다. 예를 들어, ExecutorService는 자주 사용되는 Executor 구현 중 하나입니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(10);

2. CompletableFuture에서 Executor 사용

 

CompletableFuture의 비동기 작업을 시작할 때, Executor를 두 번째 인수로 전달합니다.

 

supplyAsync 사용 예

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CompletableFutureWithExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 비동기 작업 수행
            return "Hello, World!";
        }, executor);

        future.thenAccept(result -> System.out.println("Result: " + result));

        // Executor 서비스 종료
        executor.shutdown();
    }
}

3. 여러 비동기 작업을 결합하여 Executor 사용

thenApplyAsync, thenAcceptAsync, thenRunAsync 등의 메서드도 Executor를 인수로 받아서 지정할 수 있습니다.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CompletableFutureWithMultipleAsyncExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        CompletableFuture.supplyAsync(() -> {
            // 첫 번째 비동기 작업
            return "Hello";
        }, executor).thenApplyAsync(result -> {
            // 두 번째 비동기 작업
            return result + ", World!";
        }, executor).thenAcceptAsync(result -> {
            // 세 번째 비동기 작업
            System.out.println("Result: " + result);
        }, executor);

        // Executor 서비스 종료
        executor.shutdown();
    }
}

 

요약

 

1. Executor 생성: ExecutorService와 같은 Executor 구현체를 생성합니다.

ExecutorService executor = Executors.newFixedThreadPool(10);

2. 비동기 작업에서 Executor 사용: supplyAsync, runAsync, thenApplyAsync, thenAcceptAsync, thenRunAsync 등의 메서드에서 Executor를 두 번째 인수로 전달합니다.

CompletableFuture.supplyAsync(() -> "Hello, World!", executor);

3. Executor 서비스 종료: 모든 비동기 작업이 완료되면 executor.shutdown()을 호출하여 Executor를 종료합니다.

executor.shutdown();

 

CompletableFuture Executor

CompletableFutureExecutor를 같이 사용하는 것은 여러 가지 이점을 제공합니다.

이 조합을 사용하면 비동기 작업의 효율성과 유연성을 극대화할 수 있습니다.

주요 이점

 

1. 병렬 처리의 효율성 증가

 

설명: 여러 비동기 작업을 병렬로 처리함으로써 전체 작업의 수행 시간을 단축할 수 있습니다. Executor를 사용하면 특정 스레드 풀을 통해 작업을 분산시켜 병렬 처리의 효율성을 극대화할 수 있습니다.

 

2. 작업 스케줄링 제어

 

설명: Executor를 사용하면 비동기 작업의 실행 정책을 세밀하게 제어할 수 있습니다. 예를 들어, 고정된 수의 스레드 풀, 캐시된 스레드 풀, 단일 스레드 풀 등을 사용하여 작업을 스케줄링할 수 있습니다.

 

3. 리소스 관리

 

설명: Executor를 사용하면 스레드 생성 및 관리를 중앙집중식으로 제어할 수 있어 시스템 리소스를 효율적으로 관리할 수 있습니다. 이를 통해 과도한 스레드 생성으로 인한 성능 저하를 방지할 수 있습니다.

 

4. 작업의 독립성 보장

 

설명: 각 비동기 작업을 별도의 스레드에서 실행하므로 작업 간의 간섭을 최소화할 수 있습니다. 이를 통해 독립적인 작업이 서로 영향을 주지 않고 안전하게 실행될 수 있습니다.

CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
    // 첫 번째 작업
}, executor);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
    // 두 번째 작업
}, executor);

5. 복잡한 비동기 작업 처리

 

설명: 여러 비동기 작업을 조합하여 복잡한 비동기 워크플로우를 쉽게 구현할 수 있습니다. CompletableFuture의 다양한 메서드를 사용하여 작업 간의 의존성을 설정하고, Executor를 통해 이러한 작업을 효율적으로 처리할 수 있습니다.

 

6. 예외 처리 및 복구

 

설명: 비동기 작업에서 발생하는 예외를 처리하고, 필요시 복구 작업을 수행할 수 있습니다. CompletableFutureexceptionally, handle 메서드와 함께 사용하여 예외 처리를 더 유연하게 할 수 있습니다.

 

종합 예시 코드

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CompletableFutureWithExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        CompletableFuture.supplyAsync(() -> {
            // 비동기 작업 1
            return "Task 1 result";
        }, executor).thenApplyAsync(result -> {
            // 비동기 작업 2
            return result + " + Task 2 result";
        }, executor).thenAcceptAsync(result -> {
            // 비동기 작업 3
            System.out.println("Final Result: " + result);
        }, executor).exceptionally(ex -> {
            System.err.println("Exception: " + ex);
            return null;
        });

        executor.shutdown();
    }
}

 

요약

기본 스레드 풀: CompletableFutureExecutor와 함께 사용하지 않으면, 기본적으로 ForkJoinPool의 공용 스레드 풀을 사용합니다.

공용 스레드 풀의 크기는 시스템의 가용 프로세서 수에 따라 자동으로 결정됩니다.

반응형
반응형

CompletableFuture는 자바 8에서 도입된 java.util.concurrent 패키지의 클래스입니다. 비동기 프로그래밍을 쉽게 구현할 수 있도록 다양한 메서드와 기능을 제공합니다. CompletableFuture는 비동기 작업을 수행하고, 그 결과를 비동기적으로 처리할 수 있게 해줍니다.

 

1. CompletableFuture의 기본 개념

 

비동기 프로그래밍: 메인 스레드와는 별도로 작업을 수행하여 응답성을 높입니다.

비동기 작업의 관리: 작업의 완료 여부를 확인하고, 작업이 완료되면 후속 작업을 수행합니다.

콜백 등록: 작업이 완료되면 실행할 콜백 함수를 등록할 수 있습니다.

 

2. CompletableFuture의 생성

 

CompletableFuture 객체는 여러 가지 방법으로 생성할 수 있습니다.

 

직접 생성:

CompletableFuture<String> future = new CompletableFuture<>();

 

비동기 작업을 시작하면서 생성:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "Hello, World!";
});

 

비동기 작업의 결과를 미리 정의하면서 생성:

CompletableFuture<String> future = CompletableFuture.completedFuture("Hello, World!");

 

3. 주요 메서드

 

비동기 작업 실행

 

runAsync: 결과가 없는 작업을 비동기로 실행합니다.

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("Running asynchronously");
});

 

supplyAsync: 결과가 있는 작업을 비동기로 실행합니다.

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "Hello, World!";
});

 

결과 처리

 

thenApply: 이전 작업의 결과를 받아서 변환합니다.

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
                                                    .thenApply(result -> result + ", World!");

thenAccept: 이전 작업의 결과를 받아서 소비합니다.

CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> "Hello")
                                                  .thenAccept(result -> System.out.println(result));

thenRun: 이전 작업의 결과를 사용하지 않고 실행할 작업을 정의합니다.

CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> "Hello")
                                                  .thenRun(() -> System.out.println("Task completed"));

예외 처리

 

exceptionally: 예외가 발생했을 때 대체 값을 제공합니다.

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (true) throw new RuntimeException("Error occurred");
    return "Hello";
}).exceptionally(ex -> "Recovered from error");

 

handle: 정상적인 결과와 예외를 모두 처리합니다.

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (true) throw new RuntimeException("Error occurred");
    return "Hello";
}).handle((result, ex) -> {
    if (ex != null) return "Recovered from error";
    return result;
});

 

여러 작업의 조합

 

thenCombine: 두 비동기 작업의 결과를 조합합니다.

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2);

allOf: 여러 비동기 작업을 모두 완료할 때까지 기다립니다.

CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2);

anyOf: 여러 비동기 작업 중 하나라도 완료되면 결과를 반환합니다.

CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2);

4. CompletableFuture 사용 예제

 

예제 1: 기본 사용

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
                                                            .thenApply(result -> result + ", World!")
                                                            .thenAccept(System.out::println);
    }
}

예제 2: 예외 처리

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("Error occurred");
            return "Hello";
        }).exceptionally(ex -> "Recovered from error")
          .thenAccept(System.out::println);
    }
}

예제 3: 여러 작업의 조합

import java.util.concurrent.CompletableFuture;

public class CompletableFutureCombineExample {
    public static void main(String[] args) {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

        CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2);
        combinedFuture.thenAccept(System.out::println);
    }
}

 

요약

 

비동기 프로그래밍: CompletableFuture는 비동기 작업을 쉽게 관리하고 처리할 수 있는 다양한 기능을 제공합니다.

비동기 작업 실행: runAsyncsupplyAsync를 사용하여 비동기 작업을 시작합니다.

결과 처리: thenApply, thenAccept, thenRun 등의 메서드를 사용하여 비동기 작업의 결과를 처리합니다.

예외 처리: exceptionally, handle 메서드를 사용하여 예외를 처리합니다.

여러 작업의 조합: thenCombine, allOf, anyOf를 사용하여 여러 비동기 작업을 조합합니다.

반응형
반응형

중간 연산은 스트림 파이프라인에서 데이터를 변환하고 필터링하는 작업을 수행합니다.

중간 연산은 지연 연산(lazy evaluation)으로, 최종 연산이 호출될 때까지 실제로 수행되지 않습니다. 중간 연산은 항상 새로운 스트림을 반환합니다.

 

1. filter

설명: 주어진 조건에 맞는 요소만을 포함하는 스트림을 반환합니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
Stream<String> filteredStream = items.stream().filter(item -> item.startsWith("A"));

2. map

설명: 각 요소를 주어진 함수에 의해 변환된 결과로 매핑하여 새로운 스트림을 반환합니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
Stream<String> mappedStream = items.stream().map(String::toUpperCase);

3. flatMap

설명: 각 요소를 스트림으로 변환한 후, 하나의 스트림으로 평탄화하여 반환합니다.

예시:

List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("a", "b", "c"),
    Arrays.asList("d", "e", "f")
);
Stream<String> flatMappedStream = listOfLists.stream().flatMap(List::stream);

4. distinct

설명: 스트림의 중복 요소를 제거합니다.

예시:

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
Stream<Integer> distinctStream = numbers.stream().distinct();

5. sorted

설명: 스트림의 요소를 정렬합니다.

예시:

List<String> items = Arrays.asList("Banana", "Apple", "Orange");
Stream<String> sortedStream = items.stream().sorted();

6. peek

설명: 각 요소를 소비하는 동안 추가 작업을 수행합니다. 주로 디버깅 목적으로 사용됩니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
Stream<String> peekedStream = items.stream().peek(System.out::println);

7. limit

설명: 스트림의 요소를 지정된 수만큼 제한합니다.

예시:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> limitedStream = numbers.stream().limit(3);

8. skip

설명: 스트림의 처음 N개의 요소를 건너뜁니다.

예시:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> skippedStream = numbers.stream().skip(2);

 

최종 연산 (Terminal Operations)

 

최종 연산은 스트림 파이프라인을 실행하고 결과를 생성합니다. 최종 연산은 스트림을 소비하며, 더 이상 다른 스트림 연산을 수행할 수 없습니다.

 

1. forEach

설명: 각 요소를 소비하여 주어진 작업을 수행합니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
items.stream().forEach(System.out::println);

2. collect

설명: 스트림의 요소를 컬렉션이나 다른 유형의 결과로 수집합니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
List<String> collectedList = items.stream().collect(Collectors.toList());

 

3. reduce

설명: 스트림의 요소를 결합하여 하나의 값으로 줄입니다.

예시:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);

 

4. count

설명: 스트림의 요소 개수를 반환합니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
long count = items.stream().count();

 

5. anyMatch, allMatch, noneMatch

설명: 스트림의 요소가 주어진 조건에 대해 하나라도/모두/하나도 만족하지 않는지 확인합니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
boolean anyMatch = items.stream().anyMatch(item -> item.startsWith("A"));
boolean allMatch = items.stream().allMatch(item -> item.length() > 3);
boolean noneMatch = items.stream().noneMatch(item -> item.startsWith("Z"));

6. findFirst, findAny

설명: 스트림의 첫 번째 요소를 찾거나, 임의의 요소를 찾습니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
Optional<String> firstItem = items.stream().findFirst();
Optional<String> anyItem = items.stream().findAny();

7. toArray

설명: 스트림의 요소를 배열로 반환합니다.

예시:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
String[] itemArray = items.stream().toArray(String[]::new);

 

요약

 

중간 연산: 스트림을 변환하거나 필터링하는 연산으로, 지연 연산(lazy evaluation) 특성을 가집니다. (예: filter, map, distinct, sorted)

최종 연산: 스트림을 소비하여 결과를 생성하는 연산으로, 스트림 파이프라인을 실행합니다. (예: forEach, collect, reduce, count)

반응형
반응형

스트림 API

 

스트림 API의 필요성

 

함수형 프로그래밍 지원: 자바 8에서 도입된 스트림 API는 함수형 프로그래밍을 지원하여 코드의 간결성과 가독성을 높입니다.

데이터 처리 효율성: 컬렉션 데이터의 필터링, 매핑, 정렬 등 다양한 작업을 쉽게 수행할 수 있습니다.

병렬 처리: 스트림 API는 간단한 병렬 처리를 지원하여 대량 데이터 처리의 성능을 향상시킵니다.

 

스트림 API의 기본 사용법

 

스트림 생성

  - 컬렉션에서 스트림 생성:

List<String> items = Arrays.asList("Apple", "Banana", "Orange");
Stream<String> stream = items.stream();

 

 - 배열에서 스트림 생성:

String[] array = {"Apple", "Banana", "Orange"};
Stream<String> stream = Arrays.stream(array);

 

중간 연산 (Intermediate Operations)

  - 필터링 (Filtering):

Stream<String> filteredStream = stream.filter(item -> item.startsWith("A"));

 

  - 매핑 (Mapping):

Stream<String> mappedStream = stream.map(String::toUpperCase);

 

최종 연산 (Terminal Operations)

  - 수집 (Collecting):

List<String> result = stream.collect(Collectors.toList());

 

  - 반복 (Iteration):

stream.forEach(System.out::println);

 

람다 표현식의 필요성

 

간결한 코드: 람다 표현식은 익명 함수를 표현하는 간결한 방법으로, 코드의 길이를 줄이고 가독성을 높입니다.

함수형 프로그래밍: 함수형 인터페이스와 함께 사용되어 함수형 프로그래밍 패턴을 지원합니다.

 

람다 표현식의 기본 사용법

 

기본 문법: (매개변수) -> { 함수 내용 }

(int a, int b) -> a + b;

단순 예제:

Runnable 인터페이스 구현:

Runnable r = () -> System.out.println("Hello, World!");
new Thread(r).start();

함수형 인터페이스와 함께 사용:

Predicate 인터페이스:

Predicate<String> isEmpty = s -> s.isEmpty();
boolean result = isEmpty.test("");

 

실습 예제 1: 리스트 필터링과 매핑

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<String> items = Arrays.asList("Apple", "Banana", "Orange", "Apricot");

        // 필터링: "A"로 시작하는 항목 필터링
        List<String> filteredItems = items.stream()
                                          .filter(item -> item.startsWith("A"))
                                          .collect(Collectors.toList());
        System.out.println("Filtered Items: " + filteredItems);

        // 매핑: 모든 항목을 대문자로 변환
        List<String> mappedItems = items.stream()
                                        .map(String::toUpperCase)
                                        .collect(Collectors.toList());
        System.out.println("Mapped Items: " + mappedItems);
    }
}

실습 예제 2: 숫자 리스트 처리

import java.util.Arrays;
import java.util.List;

public class LambdaExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 스트림을 사용하여 합계 계산
        int sum = numbers.stream()
                         .reduce(0, (a, b) -> a + b);
        System.out.println("Sum: " + sum);

        // 람다 표현식을 사용하여 각 숫자를 출력
        numbers.forEach(n -> System.out.println("Number: " + n));
    }
}

 

 

스트림의 특징
1. Stream 은 원본의 데이터를 변경하지 않는다. 

2. Stream 은 재 사용이 불가능하여서 일회용으로 사용된다.

3. 내부 반복으로 작업을 처리한다.

반응형
반응형

자바의 Integer와 int 자료형 차이

 

1. 기본 데이터 타입 (Primitive Type) - int

 

정의: int는 기본 데이터 타입으로, 32비트 정수를 저장합니다.

특징:

메모리 효율적: 4바이트(32비트)를 사용하여 정수 값을 저장합니다.

값 저장: 단순히 값을 저장하고 연산을 수행합니다.

기본값: 0 (선언만 하고 초기화하지 않은 경우)

성능: 오토박싱/언박싱이 필요 없기 때문에 성능이 더 좋습니다.

 

2. 참조 데이터 타입 (Reference Type) - Integer

 

정의: Integer는 자바의 래퍼 클래스(wrapper class)로, int의 객체 표현입니다.

특징:

클래스 기반: Integer 클래스는 java.lang 패키지에 속하며, 객체로 취급됩니다.

기본값: null (선언만 하고 초기화하지 않은 경우)

기능: 추가 메서드와 기능 제공 (예: parseInt, valueOf, toString 등)

오토박싱 및 언박싱: 자바 컴파일러가 기본 타입과 래퍼 클래스 간의 자동 변환을 수행합니다.

 

Integer x = 10; // 오토박싱
int y = x;      // 언박싱
Integer z = Integer.valueOf(20);
int sum = x + z;

3. 차이점 요약

 

4. 언제 사용해야 하나?

int 사용 시점:

- 성능이 중요한 경우

- 단순히 숫자 값을 저장하고 연산할 때

Integer 사용 시점:

- 객체로 처리해야 할 때 (예: 컬렉션 프레임워크와 함께 사용)

- null 값을 허용해야 할 때

- 메서드와 추가 기능이 필요할 때

 

 

5. 기본 데이터 타입과 래퍼 클래스의 다른 예

 

자바에는 다양한 기본 데이터 타입과 그에 대응하는 래퍼 클래스가 있습니다. 각 래퍼 클래스는 기본 데이터 타입을 객체로 다룰 수 있도록 하며, 추가적인 메서드와 기능을 제공합니다.

 

 기본 데이터 타입과 그에 대응하는 래퍼 클래스

 

요약

 

기본 데이터 타입은 메모리 효율적이고 성능이 좋지만, 객체로 다룰 수 없으며 null 값을 가질 수 없습니다. 반면 래퍼 클래스는 추가적인 메서드와 기능을 제공하며 컬렉션과 같은 객체 기반 데이터 구조에서 사용할 수 있습니다. 따라서 상황에 따라 적절한 타입을 선택하는 것이 중요합니다.

반응형

+ Recent posts