📦 Redis (Remote Dictionary Server) 란?

오픈 소스 인메모리 데이터 구조 저장소.

❓인메모리란?

데이터를 디스크와 같은 비휘발성 저장 장치가 아닌 주 메모리(RAM)와 같은 휘발성 메모리에 저장하는 방식.

- 빠른 데이터 접근 : 빠른 데이터 처리 속도 제공
- 휘발성 : 전원이 꺼지면 데이터가 사라짐
- 인메모리 데이터베이스 : 데이터를 메모리에 저장하여 빠른 속도로 데이터를 조회하고 처리함 (예 : Redis, Memcached, SAP HANA )

 

  1. 인메모리 저장소:
    • Redis는 데이터를 메모리에 저장하기 때문에 매우 빠른 읽기/쓰기 성능을 제공한다. 이는 디스크 기반 데이터베이스보다 훨씬 빠른 성능을 제공하므로, 캐싱, 세션 관리, 실시간 분석 등 고속 처리가 필요한 애플리케이션에 적합하다.
  2. 다양한 데이터 구조 지원:
    • Redis는 단순한 키-값 저장소 이상으로, 다양한 데이터 구조를 지원한다. 이를 통해 복잡한 데이터를 손쉽게 저장하고 조작할 수 있다.
  3. 영속성 옵션:
    • Redis는 기본적으로 인메모리 데이터 저장소이지만, 데이터를 디스크에 저장하여 영속성을 보장할 수 있습니다. 이를 위해 RDB(Snapshotting)와 AOF(Append-Only File) 두 가지 방식이 제공됩니다.
    • RDB: 특정 간격으로 데이터를 스냅샷으로 저장하는 방식
    • AOF: 모든 쓰기 작업을 로그로 기록하여 장애 시 복구하는 방식
  4. 복제 및 클러스터링:
    • Redis는 데이터 복제를 통해 고가용성을 제공할 수 있다. 마스터-슬레이브 구조를 사용하여 데이터를 복제하며, 슬레이브 노드는 읽기 작업을 분산할 수 있다.
    • Redis 클러스터는 데이터 샤딩을 통해 여러 노드에 데이터를 분산 저장하고, 각 노드가 샤드의 일부를 관리한다. 이를 통해 수평 확장이 가능하며, 대규모의 데이터를 처리할 수 있다.
  5. 트랜잭션 지원:
    • Redis는 간단한 트랜잭션 메커니즘을 제공하여, 여러 명령어를 원자적으로 실행할 수 있다. MULTI, EXEC, WATCH 명령어를 사용하여 트랜잭션을 구현할 수 있다.

Redis 설정

의존성 추가 - build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'

 

Redis 설정 파일 - RedisRepositoryConfig.java

public class RedisRepositoryConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Value("${spring.data.redis.password}")
    private String password;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPassword(password);
        redisStandaloneConfiguration.setPort(port);

        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }
}

 

application.yml 파일 설정

spring:
  data:
    redis:
      host: localhost
      port: 6379

 

Redis에 토큰을 저장하는 기능 추가 - JwtUtil.java

// Redis에 접근하기 위한 RedisTemplate을 추가
@Getter
private final RedisTemplate<String, String> redisTemplate;

public JwtUtil(RedisTemplate<String, String> redisTemplate) {
    this.redisTemplate = redisTemplate;
}
public UserResponse.tokenInfo generateTokens(String userId) {
    String accessToken = createAccessToken(userId);
    String refreshToken = createRefreshToken(userId);

    // Redis에 토큰 저장
    saveTokenToRedis(userId, accessToken, refreshToken);

    UserResponse.tokenInfo tokenInfo = UserResponse.tokenInfo.builder()
            .grantType("Bearer")
            .accessToken(accessToken)
            .refreshToken(refreshToken)
            .build();

    return tokenInfo;
}
private void saveTokenToRedis(String userId, String accessToken, String refreshToken) {
    redisTemplate.opsForValue().set("ACCESS_TOKEN:" + userId, accessToken, accessTokenValidity, TimeUnit.MILLISECONDS);
    redisTemplate.opsForValue().set("REFRESH_TOKEN:" + userId, refreshToken, refreshTokenValidity, TimeUnit.MILLISECONDS);
}
public String refreshAccessToken(String refreshToken) {
    if (validateToken(refreshToken)) {
        Claims claims = getClaimsFromToken(refreshToken);
        String userId = claims.get("userId", String.class);

        //Redis에서 refreshToken 검증
        String redisRefreshToken = redisTemplate.opsForValue().get("REFRESH_TOKEN:" + userId);
        if (redisRefreshToken != null && redisRefreshToken.equals(refreshToken)) {
            return createAccessToken(userId);
        }
    }
    return null;
}

 

Redis와의 연동 - JwtInterceptor.java

@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {
    private final JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws IOException {

        String accessToken = request.getHeader(TokenType.ACCESS_TOKEN.toString());
        String refreshToken = request.getHeader(TokenType.REFRESH_TOKEN.toString());

        if (accessToken != null && jwtUtil.validateToken(accessToken)) {
            String userId = jwtUtil.getUserIdFromToken(accessToken);
            // 토큰 유효성을 검사할 때 Redis에 저장된 토큰과 비교하여 검증
            String redisAccessToken = jwtUtil.getRedisTemplate().opsForValue().get("ACCESS_TOKEN:" + userId);
            if (redisAccessToken != null && redisAccessToken.equals(accessToken)) {
                return true;
            }
        }

        if (refreshToken != null && jwtUtil.validateToken(refreshToken)) {
            String newAccessToken = jwtUtil.refreshAccessToken(refreshToken);
            if (newAccessToken != null) {
                response.setHeader(TokenType.ACCESS_TOKEN.toString(), newAccessToken);
                return true;
            }
        }

        response.setStatus(401);
        return false;
    }

}

 


Redis 테스트 

Redis 실행

 

Postman에서 로그인 후 토큰 확인

 

Redis 데이터 확인 - redis.cli

 

AWS CloudFront 란

AWS CloudFront는 AWS의 CDN (Content Delivery Network) 서비스다. CDN 서비스란 클라이언트의 콘텐츠 요청으로 서버에서 받아온 콘텐츠를 캐싱하고 이후 같은 요청이 왔을 때, 캐싱해 둔 콘텐츠를 제공하는 서비스다. 물리적으로 거리가 먼 곳에도 빠르게 요청을 처리할 수 있고 결과적으로 서버의 부하를 낮출 수 있다.

 

Edge Location(Pop) / Regional Edge Caceh(REC)

Edge Location

CloudFront 서비스가 콘텐츠를 캐싱하고 클라이언트에게 제공하는 지점 혹은 캐시 서버, 전 세계 주요 도시에 분포

사용자가 요청한 콘텐츠의 캐시가 Edge Location에 있다면 멀리 있는 서버에 직접 요청이 아닌 가까운 Edge Location에 저장된 캐시를 불러올 수 있다.

 

Regional Edge Cace

사용자가 접근할 수 있게 배포되어 있는 CloudFront 위치, CloudFront가 오리진에 요청하는것을 줄여준다.

캐시가 더 오랫동안 남고 Edge Location 보다 캐시 스토리지 용량이 더 크다.

 

CloudFront 동작 순서

  1. 사용자가 어플리케이션에 요청을 한다.
  2. DNS는 사용자에게 적합한 Edge Location으로 라우팅 한다.
  3. Edge Location에서 캐시를 확인하고 있으면 사용자에게 반환한다.
  4. 없으면 가장 가까운 REC로 캐시가 있는지 확인을 요청한다.
  5. 없으면 오리진으로 요청을 전달한다.
  6. 오리진 > REC > Edge Location > CloudFront가 사용자에게 전달 (캐시도 추가된다.)
  7. REC에 캐시가 있다면 REC는 콘텐츠를 요청한 Edge Location은 이를 사용자에게 반환한다.
  8. Edge Location은 나중을 위해 이 콘텐츠 캐시를 저장한다.

* 오리진 : 콘텐츠가 위치하고 있는 근원(ex. S3 버킷 등)

* DNS (Domain Name System) : 도메인 이름을 ip주소로 변환해주는 시스템

* 라우팅 : 데이터 패킷을 네트워크를 통해 목적지까지 전달하는 과정에서 최적의 경로를 결정하고 그 경로로 패킷을 전송하는 것


S3 이미지 업로드 + CloudFront 이미지 캐싱

 

✅ AWS ClodFront 배포 생성 및 권한 설정

CloudFront 배포 생성 클릭

 

Origin domain 항목에서 생성한 S3 도메인 선택

 

 

 

 

 

 

 

✔ 위와 같이 설정 완료한 뒤 배포 생성 클릭!

 

 

버킷 메뉴 - 권한 - 버킷 정책에 복사한 정책 붙여넣기

 

 

✅ 코드 수정

UserService.java

@Transactional
public String upload(String userId, MultipartFile image) {
    String profileImage = s3ImageService.upload(image);
    String imageCaching = "https://{CloudFront 도메인 이름}/"+profileImage.split("/")[profileImage.split("/").length-1];
    userDao.uploadProfile(userId, profileImage, imageCaching);

    return userId;
}

@Transactional
public String updateProfile(String userId, MultipartFile image) {
    String imageUrl = userDao.findById(userId).imageUrlS3();
    s3ImageService.deleteImageFromS3(imageUrl);

    return upload(userId, image);
}

 

프로필 이미지를 업로드하고 수정하는 메서드에 CloudFront url을 생성해 DB에 저장하고 조회하는 로직으로 수정

S3 이미지를 업로드 프로세스


  1. 클라이언트에서 이미지를 업로드하면 서버가 이를 수신해 MultipartFile 객체로 처리한다.
  2. MultipartFile을 File 객체로 변환하고 S3 클라이언트를 사용하여 변환된 파일을 S3 버킷에 업로드한다. 여기서 AWS S3 버킷의 이름과 업로드할 파일의 이름을 지정한다.
  3. 업로드된 파일은 S3 버킷에 저장되며, 해당 파일에 접근할 수 있는 URL을 생성하여 클라이언트에게 반환한다. 이 URL은 클라이언트가 파일을 접근하거나 다운로드할 수 있도록 한다. 이 URL을 db에 저장해서 필요할 때 이 URL로 이미지 데이터를 사용할 수 있다.

 

Spring Boot + Gradle 프로젝트 - S3 파일 업로드


✅ build.gradle

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

✅ application.yml

accessKey와 secretKey를 application.yml에 등록해야 하는데 이 Key는 외부에 노출되서는 안된다!!!!

이 Key를 이용하여 누군가 악용하면 AWS 계정에 비용이 청구될 수 있다.

 

따라서 application.yml 파일에 설정을 추가해 주고, application-private.yml 파일을 생성하고 . gitignore 파일에 등록하여 github에 올라가지 않도록 설정해야 한다.

application.yml

 

application-private.yml

 

.gitignore

✅ S3Config.java

ackage com.example.fitwithme.common;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;
    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;
    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 amazonS3() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return AmazonS3ClientBuilder
                .standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();
    }
}

 

✅ S3ImageService.java

package com.example.fitwithme.application.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.util.IOUtils;
import com.example.fitwithme.common.exception.S3ErrorStatus;
import com.example.fitwithme.common.exception.S3Exception;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

@RequiredArgsConstructor
@Component
public class S3ImageService {
    private final AmazonS3 amazonS3;

    @Value("${cloud.aws.s3.bucketName}")
    private String bucketName;

    public String upload(MultipartFile image) {
        if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){
            throw new S3Exception(S3ErrorStatus.EMPTY_FILE_EXCEPTION);
        }
        return this.uploadImage(image);
    }

    private String uploadImage(MultipartFile image) {
        this.validateImageFileExtension(image.getOriginalFilename());
        try {
            return this.uploadImageToS3(image);
        } catch (IOException e) {
            throw new S3Exception(S3ErrorStatus.IO_EXCEPTION_ON_IMAGE_UPLOAD);
        }
    }

    private void validateImageFileExtension(String filename) {
        int lastDotIndex = filename.lastIndexOf(".");
        if (lastDotIndex == -1) {
            throw new S3Exception(S3ErrorStatus.NO_FILE_EXTENSION);
        }

        String extension = filename.substring(lastDotIndex + 1).toLowerCase();
        List<String> allowedExtensionList = Arrays.asList("jpg", "jpeg", "png", "gif");

        if (!allowedExtensionList.contains(extension)) {
            throw new S3Exception(S3ErrorStatus.INVALID_FILE_EXTENSION);
        }
    }

    private String uploadImageToS3(MultipartFile image) throws IOException {
        String originalFilename = image.getOriginalFilename(); //원본 파일 명
        String extention = originalFilename.substring(originalFilename.lastIndexOf("."));

        String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename;

        InputStream is = image.getInputStream();
        byte[] bytes = IOUtils.toByteArray(is);

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("image/" + extention);
        metadata.setContentLength(bytes.length);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try{
            PutObjectRequest putObjectRequest =
                    new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata)
                            .withCannedAcl(CannedAccessControlList.PublicRead);
            amazonS3.putObject(putObjectRequest);
        }catch (Exception e){
            throw new S3Exception(S3ErrorStatus.PUT_OBJECT_EXCEPTION);
        }finally {
            byteArrayInputStream.close();
            is.close();
        }

        return amazonS3.getUrl(bucketName, s3FileName).toString();
    }

    public void deleteImageFromS3(String imageAddress){
        String key = getKeyFromImageAddress(imageAddress);
        try{
            amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
        }catch (Exception e){
            throw new S3Exception(S3ErrorStatus.IO_EXCEPTION_ON_IMAGE_DELETE);
        }
    }

    private String getKeyFromImageAddress(String imageAddress){
        try{
            URL url = new URL(imageAddress);
            String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
            return decodingKey.substring(1);
        }catch (MalformedURLException | UnsupportedEncodingException e){
            throw new S3Exception(S3ErrorStatus.IO_EXCEPTION_ON_IMAGE_DELETE);
        }
    }
}

 

🔹upload(MultipartFile image)

넘겨받은 이미지 파일이 비어있는 파일인지 검증한다.

uploadImage를 호출하여 S3에 저장된 이미지 객체의 public url을 반환한다.

 

🔹uploadImage(MultipartFile image)

validateImageFileExtension을 호출하여 확장자 명이 올바른지 확인한다.

uploadImageToS3를 호출하여 이미지를 S3에 업로드하고 업로드된 이미지의 public url을 받아서 서비스 로직에 반환한다.

 

🔹validateImageFileExtension(String filename)

filename을 받아서 파일 확장자가  jpg, jpeg, png, gif 중에 속하는지 검증한다.

 

🔹uploadImageToS3(MultipartFile image)

S3에 이미지를 업로드한다.

private String uploadImageToS3(MultipartFile image) throws IOException {
  String originalFilename = image.getOriginalFilename(); //원본 파일 명
  String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명

  String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //변경된 파일 명

  InputStream is = image.getInputStream();
  byte[] bytes = IOUtils.toByteArray(is); //image를 byte[]로 변환

  ObjectMetadata metadata = new ObjectMetadata(); //metadata 생성
  metadata.setContentType("image/" + extension);
  metadata.setContentLength(bytes.length);
  
  //S3에 요청할 때 사용할 byteInputStream 생성
  ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); 

  try{
    //S3로 putObject 할 때 사용할 요청 객체
    //생성자 : bucket 이름, 파일 명, byteInputStream, metadata
    PutObjectRequest putObjectRequest =
        new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata)
            .withCannedAcl(CannedAccessControlList.PublicRead);
            
    //실제로 S3에 이미지 데이터를 넣는 부분이다.
    amazonS3.putObject(putObjectRequest); // put image to S3
  }catch (Exception e){
    throw new S3Exception(ErrorCode.PUT_OBJECT_EXCEPTION);
  }finally {
    byteArrayInputStream.close();
    is.close();
  }

  return amazonS3.getUrl(bucketName, s3FileName).toString();
}

 

🔹deleteImageFromS3(String imageAddress)

이미지의 public url을 이용해서 S3에서 해당 이미지를 삭제한다.

getKeyFromImageAddress를 호출해서 삭제에 필요한 key를 가져온다.

 

🔹getKeyFromImageAddress(String imageAddress)

이미지의 url을 받아와 디코딩한 url을 맨 앞의 '/'를 제거한 key를 반환한다.

 

✅ S3Exception.java

package com.example.fitwithme.common.exception;

public class S3Exception extends RuntimeException{
    private final S3ErrorStatus status;

    public S3Exception(S3ErrorStatus status) {
        super(status.getMessage());
        this.status = status;
    }

    public S3ErrorStatus getStatus() {
        return this.status;
    }
}

 

✅ S3ErrorStatus.java

package com.example.fitwithme.common.exception;

public enum S3ErrorStatus {
    OK("Success"),
    EMPTY_FILE_EXCEPTION("첨부된 이미지가 없습니다."),
    IO_EXCEPTION_ON_IMAGE_UPLOAD("IOException이 발생했습니다."),
    NO_FILE_EXTENSION("잘못된 파일 형식입니다."),
    INVALID_FILE_EXTENSION("이미지 파일 형식이 아닙니다."),
    PUT_OBJECT_EXCEPTION("이미지 업로드에 실패했습니다."),
    IO_EXCEPTION_ON_IMAGE_DELETE("이미지 삭제에 실패했습니다.");

    private final String message;

    S3ErrorStatus(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

}

 

✅ UserController.java

@PostMapping("/Upload-profile")
public ResponseEntity<String> profileUpload(@RequestHeader("ACCESS_TOKEN") String accessToken, @RequestPart(value = "image", required = false) MultipartFile image){
    String userId = jwtUtil.getUserIdFromToken(accessToken);
    String profileImage = userService.upload(userId, image);
    return ResponseEntity.ok(profileImage);
}
    
@PutMapping("/update-profile")
public ResponseEntity<String> updateProfile(@RequestHeader("ACCESS_TOKEN") String accessToken, @RequestPart(value = "image", required = false) MultipartFile image){
    String userId = jwtUtil.getUserIdFromToken(accessToken);
    String profileImage = userService.updateProfile(userId, image);
    return ResponseEntity.ok(profileImage);
}

 

✅ UserService.java

@Transactional
public String upload(String userId, MultipartFile image) {
        String profileImage = s3ImageService.upload(image);
        userDao.uploadProfile(userId, profileImage);

        return userId;
    }
    
@Transactional
public String updateProfile(String userId, MultipartFile image) {
    String imageUrl = userDao.findById(userId).imageUrl();
    s3ImageService.deleteImageFromS3(imageUrl);

    String profileImage = s3ImageService.upload(image);
    userDao.uploadProfile(userId, profileImage);

    return userId;
}

 

이미지 업로드 시 s3 ImageService를 이용하여 이미지를 저장한 뒤, 반환받은 public url을 DB에 저장하는 로직으로 처리했다.


☝ S3를 사용해 이미지를 업로드할 경우의 장점

  1. 내구성과 안전성 : AWS의 S3는 데이터의 내구성과 안정성을 보장합니다. 데이터가 여러 복제본에 걸쳐 저장되므로 데이터 손실 우려가 줄어듭니다.
  2. 확장성 : S3는 거의 무한한 확장성을 제공하므로 대용량 데이터를 저장하고 처리할 수 있습니다.
  3. 다양한 데이터 관리 기능 : 버전 관리, 암호화, 액세스 제어 등 다양한 데이터 관리 기능을 제공하여 데이터를 보다 효율적으로 관리할 수 있습니다.
  4. 저렴한 비용 : S3는 사용한 만큼만 비용을 지불하므로 비용을 절감할 수 있습니다.

Amazon S3에서 로그인하고 버킷 만들기 클릭

 

버킷 생성

 

IAM 생성

 

IAM accessKey, secretKey 얻기

액세스 키와 비밀 액세스 키 저장해두기! → spring properties에 등록해서 S3 bucket에 접근하는 데에 사용

비밀 액세스 키 (secretKey)는 생성 시에만 검색할 수 있기 때문에 미리 저장해둬야 한다!!!

csv 파일 다운로드를 클릭해서 파일로도 저장 가능!

1. 도커 네트워크란 무엇이고, 종류로 무엇이 있는지?

🐳도커 네트워크란?

docker 컨테이너 간의 통신을 관리하고 격리하기 위한 기능을 제공하는 것

 

docker는 컨테이너화된 애플리케이션을 배포하고 관리하는 데 사용되는 오픈 소스 플랫폼이다. 이러한 컨테이너는 격리된 환경에서 실행되며, 이는 각 컨테이너가 자체 네트워크 인터페이스와 IP  주소를 가질 수 있음을 의미한다. 

컨테이너화된 애플리케이션은 여러 개의 컨테이너로 구성될 수 있는데, 이들 컨테이너가 서로 통신하고 데이터를 주고받아야 할 경우 도커 네트워크를 통해 이러한 통신을 쉽게 설정하고 관리할 수 있도록 도와준다. 컨테이너 간의 통신, 호스트와 컨테이너 간의 통신, 외부 네트워크와의 통신을 가능하게 한다.

 

 

🐳도커 네트워크의 종류

  • bridge 네트워크
    • 기본 네트워크 드라이브, 도커가 자동으로 생성하는 기본 네트워크
    • 동일한 호스트 내에서 네트워크 설정
    • 동일 호스트 내의 컨테이너 간 통신 또는 호스트와 컨테이너 간의 통신에 사용된다.
  • host 네트워크
    • 호스트의 네트워킹을 직접 사용하여 컨테이너 간의 네크워크 격리 제거
    • 호스트에서 제공하는 IP를 직접 할당하여 사용할 수 있다.
    • 컨테이너가 호스트의 네트워크 리소스에 직접 접근할 수 있기 때문에 보안 문제가 발생할 수 있다.
    • 네트워크 격리가 필요하지 않은 경우에 유용하다.
  • none 네트워크
    • 네트워크 연결이 없는 상태로 컨테이너를 실행한다.
    • 완전히 독립적인 환경을 원할 때 사용한다.
  • overlay 네트워크
    • 여러 도커 데몬을 연결하고, Docker Swarm 서비스와 컨테이너 간에 노드 간 통신을 가능하게 한다.
    • Swarm 모드에서 작동하며, 여러 호스트에 걸쳐 있는 컨테이너 간에 통신할 수 있도록 한다.

2. IoC vs DI

🌸Spring이란?

자바 언어로 엔터프라이즈급 개발을 편리하게 만들어주는 오픈소스 경량급 애플리케이션 프레임워크로, 애플리케이션 개발에 필요한 기반을 제공해서 개발자가 비즈니스 로직 구현에만 집중할 수 있도록 하는 것

 

🍔IoC(Inversion of Control) : 제어의 역전

사용할 객체를 직접 사용하지 않고, 객체의 생명주기 관리를 외부(스프링 컨테이너)에 위임하는 것

 

제어의 역전을 통해 의존성 주입, 관점 지향 프로그래밍이 가능해진다. 이에 따라 개발자는 객체의 제어권을 컨테이너로 넘기고 객체의 생명 주기 관리 등의 복잡한 요소들을 신경 쓰지 않고, 비즈니스 로직에만 집중할 수 있게 된다.

 

🍟DI(Dependency Injection) : 의존관계 주입

제어 역전의 방법 중 하나로, 사용할 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용하는 방식

 

롬복을 이용한 생성자 주입이 가장 좋은 의존성 주입 방법이다.

롬복의 @RequiredArgsConstructor 어노테이션은 final이 붙은 필드의 생성자를 자동으로 생성해 준다.

@RestController
@RequiredArgsConstructor
public class DIController {

    privite final MyService myService;
    
	//Controller Codes
}

 

3. Doker vs 가상머신

💻가상머신

  • 가상머신은 하이퍼바이저가 호스트 OS 위에서 여러 게스트 OS를 실행한다.
  • 각 VM은 자체 운영체제를 포함하며, 그 위에 애플리케이션이 실행된다.
  • VM은 하드웨어 수준에서 가상화되며 각각 독립된 운영체제를 가진다.

🐳Docker

  • 호스트 OS의 커널을 공유하며 컨테이너는 사용자 공간에서 독립된 환경을 제공한다.
  • 컨테이너는 OS 레벨에서 가상화되며 독립된 파일 시스템, 네트워크 인터페이스를 가진다.
  • 각 컨테이너는 애플리케이션과 그 종속성만을 포함한다.

4. Docker File vs Docker Image vs Docker Container

💚Docker File

서버 운영 기록을 코드화한 것

💛Docker Image

운영 기록을 실행할 시점

💙Docker Container

이미지에 실행 시점에 수정되어야 할 정보들을 더한 것

이미지를 실행해서 만들어지는 결과물 

'공부 > f-lab' 카테고리의 다른 글

f-lab 14주차  (0) 2024.06.18
f-lab 11주차  (0) 2024.05.28
f-lab 10주차  (0) 2024.05.23
f-lab 9주차  (0) 2024.05.19
f-lab 8주차  (0) 2024.05.07

1. JDBC가 어댑터 패턴을 어떻게 적용하고 있는지?

JDBC에서 데이터베이스 드라이버가 어댑터 패턴을 사용하여 구현되는데, 각 데이터베이스 벤더는 JDBC 표준 인터페이스를 구현한 드라이버를 제공하며 이 드라이버가 어댑터 역할을 한다. 이로 인해 자바 어플리케이션은 다양한 데이터베이스와 상호작용 할 수 있다.

 

  • JDBC 표준 인터페이스 : Connection, Statement, ResultSet 등
  • 데이터베이스 드라이버 : 드라이버는 어댑터 패턴을 사용하여 JDBC 인터페이스 호출을 해당 데이터베이스에 맞는 네이티브 호출로 변환
  • 드라이버 관리 : DriverManager 클래스는 어플리케이션이 데이터베이스에 연결을 요청하면 DriverManager 는 url을 기반으로 적절한 드라이버를 찾고 연결을 설정

이로 인해 JDBC는 데이터베이스 독립성을 유지하면서도 다양한 벤더의 데이터베이스와 호환될 수 있는 유연성을 갖추게 된다.

 

2. 트랜잭션 vs 락

 트랜잭션

 

데이터베이스의 일관성과 무결성 유지

데이터베이스에서 수행되는 일련의 작업 단위

  • 원자성 : 트랜잭션 내의 작업이 모두 성공하거나 모두 실패하도록 보장. 여러 작업 중 하나라도 실패하면 전체 트랜잭션이 취소되고 데이터베이스는 트랜잭션이 시작되기 전 상태로 복구된다.
  • 일관성 : 트랜잭션이 성공적으로 완료된 후 데이터베이스의 상태는 모든 정의된 규칙(무결성 제약 조건)을 만족해야 한다.
  • 격리성 : 동시에 여러 트랜잭션이 실행될 때 각 트랜잭션은 서로의 중간 상태를 확인할 수 없다. 두 트랜잭션이 같은 데이터를 수정하려고 할 때, 하나의 트랜잭션이 완료될 때까지 다른 트랜잭션은 그 데이터를 변경할 수 없다.
  • 지속성 : 트랜잭션이 성공적으로 완료되면 그 결과가 영구적으로 데이터베이스에 반영되어야 한다. 시스템 장애가 발생해도 트랜잭션 결과는 손실되지 않는다.

@Transactional 어노테이션

  • 적용 대상 : 메서드 또는 클래스 레벨에 적용
  • 기본 동작 : 트랜잭션을 시작하고 메서드가 정상적으로 완료되면 커밋, 예외가 발생하면 롤백을 한다. 

 

⛔ 락

 

동시에 여러 트랜잭션이 동일한 데이터에 접근하는 경우 데이터의 무결성과 일관성을 유지하기 위해 데이터베이스에서 사용되는 메커니즘

  • 공유 락 : 데이터에 대한 읽기 작업을 허용하지만 해당 데이터를 수정할 수 없도록 한다.
  • 배타 락 : 데이터에 대한 읽기 및 쓰기 작업을 모두 허용하지 않는다.

 

3. Spring AOP에서 프록시 패턴을 어떻게 사용하는지?

Spring AOP (Aspect-Oriented Programming)

애플리케이션의 주요 비즈니스 로직과 부가 기능을 분리하여 모듈화할 수 있는 프로그래밍 패러다임.

로깅, 트랜잭션 관리, 보안 등과 같은 공통 관심사를 핵심 비즈니스 로직과 분리하여 코드의 가독성, 유지보수성, 재사용성 향상.

  • Aspect : 공통 관심사를 모듈화한 것(로깅, 트랜잭션 관리, 보안)
  • Join Point : Aspect 가 적용될 수 있는 지점. 주로 메서드 호출 시점 
  • Pointcut : 특정 Join Point를 선택하는 표현식. Aspect 가 적용될 지점을 정의
  • Advice : Aspect 가 Join Point에서 수행하는 동작. 메서드 호출 전후/예외 발생 시점 등에 실행
  • Weaving : Aspect 를 대상 객체에 적용하는 과정. 주로 런타임 Weaving을 사용  

 Spring AOP에서 프록시 패턴 사용

Spring AOP는 런타임에 프록시 객체를 생성하여  Aspect을 메인 비즈니스 로직에 적용한다.

  • JDK 동적 프록시 : 인터페이스 기반 프록시 생성. 타겟 클래스가 구현한 인터페이스를 통해 프록시 생성
  • CGLIB 프록시 : 인터페이스가 없는 클래스에도 프록시 생성 가능. 클래스 상속을 통해 프록시 생성

서비스 객체가 빈으로 등록이 되면 스프링이 AbstractAutoProxyCreator라는 BeanPostProcessor (어떤 Bean이 등록되면, 그 Bean을 가공할 수 있는 life-cycle interface)로 서비스 빈을 감싸는 프록시 객체(빈)를 만들어 그 프록시 객체를 서비스 대신에 등록해서 사용한다.

 

4. 프록시 서버란 무엇인지?

➡ 클라이언트와 실제 서버 간의 중개자 역할을 하는 서버

     클라이언트의 요청을 받아 실제 서버에 전달하고, 실제 서버의 응답을 받아 클라이언트에 전달한다.

 

 주요 기능

  • 트래픽 제어 및 관리 : 특정 웹사이트에 대한 접근 제한, 특정 유형의 트래픽 필터링
  • 캐싱 : 자주 요청되는 웹 페이지나 파일을 캐싱하여 서버 부하를 줄이고 응답 시간 단축
  • 보안 및 익명성 제공 : 클라이언트의 IP 주소를 숨기고 실제 서버와의 직접적인 접촉을 막음으로써 보안과 익명성 제공
  • 콘텐츠 필터링 :  특정 콘텐츠에 대한 접근 제한, 악성 웹사이트 차단
  • 로드 밸런싱 : 여러 서버에 걸쳐 트래픽을 분산시켜 서버 부하를 균형있게 분배하여 서버 성능 최적화 

📶 종류

  • 포워드 프록시 : 클라이언트가 인터넷에 접속할 때 사용. 클라이언트의 요청을 받아 인터넷으로 전달하고 인터넷의 응답을 클라이언트에 전달한다. 주로 보안 및 익명성을 위해 사용된다.
  • 리버스 프록시 : 인터넷에서 들어오는 요청을 받아 내부 서버로 전달하고 내부 서버의 응답을 받아 클라이언트에 전달. 주로 로드 밸런싱, 보안, 캐싱을 위해 사용된다.
  • 웹 프록시 : 웹 트래픽을 중개. 클라이언트가 퉵 사이트에 접속할 때 익명성을 유지할 수 있다.
  • 오픈 프록시 : 누구나 사용할 수 있는 공개 프록시 서버. 익명성을 제공하지만 보안상 문제가 있을 수 있다.

 

5. 자바에서 싱글톤을 구현하는 방식이 무엇이 있는지?(즉시 로딩, 지연 로딩, syncronized, double-checked locking, lazy holder, enum 키워드 흐름대로)

🔵 즉시로딩

클래스가 로드될 때 인스턴스를 생성

인스턴스가 사용되지 않더라도 항상 메모리에 올라와있다는 단점이 있다.

public class Singleton {
    // 클래스 로드 시 인스턴스 생성
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

 

🔵 지연 로딩

인스턴스가 처음 필요할 때 생성

멀티스레드 환경에서는 인스턴스가 여러 개 생성될 수 있어 문제가 발생할 수 있다.

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 인스턴스 필요 시 생성
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

🔵 synchronized

지연 로딩 방식에서 스레드 안정성을 보장할 수 있지만 메서드 전체에 동기화를 적용하면 블로킹으로 인해 성능이 저하될 수 있다.

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // synchronized 키워드를 사용
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

🔵 Double-Checked Locking

synchronized 키워드 사용의 성능 저하 문제를 해결하기 위한 방법

인스턴스가 이미 생성된 경우는 동기화 블록을 통과하지 않도록 한다.

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        // 인스턴스가 이미 생성된 경우는 동기화 블록 통과하지 않음
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

 

🔵 Lazy Holder

내부 정적 클래스와 클래스 로더의 매커니즘을 이용한 지연 초기화 방식

내부 정적 클래스는 클래스가 로드될 때 인스턴스가 생성되지 않고 외부 클래스가 처음으로 참조될 때 초기화되는 특징

JVM은 클래스 로딩과 초기화를 스레드 안전하게 처리하기 때문에 별도의 동기화가 필요하지 않음

public class Singleton {
    private Singleton() {}

    // 내부 정적 클래스 : 싱글톤 인스턴스를 가지고 있음
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //외부에서 호출할 수 있는 메서드 : 싱글톤 인스턴스를 반환
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

 

🔵 Enum

enum 타입은 고정된 인스턴스를 가지기 때문에 싱글톤 패턴과 매우 잘 맞다.

JVM에서 싱글톤 인스턴스가 스래드 안전하게 초기화되도록 보장 

//enum 클래스 사용
public enum Singleton {
    INSTANCE;

    public void someMethod() {
        // 비즈니스 로직
    }
}

 

6. 콜백이란?

 프로그램 내에서 특정 이벤트가 발생했을 때 호출되는 함수나 메서드  

다른 함수에 인수로 전달되며 특정 조건이 만족되거나 이벤트가 발생하면 호출된다.

다른 코드의 인수로서 넘겨주는 실행 가능한 코드로 콜백을 넘겨받는 코드는 필요 따라 즉시 실행할 수도 있고 나중에 실행할 수도 있다.

자바에서는 주로 인터페이스와 람다식을 사용하여 콜백 구현 

  • 비동기 작업 후에 특정 작업을 수행하기 위해 콜백 사용
  • 한 함수가 다른 함수의 결과에 따라 동작을 달리하기 위해 사용 
  •  

7. 템플릿 메서드 패턴 vs 템플릿 콜백 패턴

템플릿 콜백 패턴은 템플릿 메서드 패턴과 유사하지만 공통 로직이 아닌 하위 클래스에서 구체화된 코드를 호출하는 부분을 자바8 이전에서는 익명 클래스로 구현하고 자바8 이후에서는 람다식으로 구현한다.

'공부 > f-lab' 카테고리의 다른 글

f-lab 17주차  (0) 2024.07.09
f-lab 11주차  (0) 2024.05.28
f-lab 10주차  (0) 2024.05.23
f-lab 9주차  (0) 2024.05.19
f-lab 8주차  (0) 2024.05.07

1. UML이란 무엇일까요?

  • Unified Modeling Language
  • 객체지향 소프트웨어 개발 및 시스템 설계를 위한 표준화된 모델링 언어
  • 소프트웨어 시스템의 구조와 행동을 시각적으로 표현하는 데 사용된다.
  • 구조 다이어그램
    • 클래스
    • 객체
    • 컴포넌트 : 시스템의 물리적 구성 요소와 그 관계
    • 배치 : 시스템의 하드웨어 구성 요소와 소프트웨어 구성 요소의 배치
  • 행위 다이어그램
    • 유스케이스
    • 시퀀스 : 객체 간의 상호작용을 시간 순서에 따라
    • 상태 : 객체의 상태 변화와 상태 간의 전이
    • 활동 : 프로세스 흐름이나 활동
  • 상호작용 다이어그램
    • 시퀀스
    • 커뮤니케이션
    • 협력

 

2. UML은 어떨 때 쓰고, 쓰면 왜 좋을까요?

  • 용도
    • 소프트웨어 개발 과정에서 시스템 구조, 동작 및 상호작용을 시각적으로 모델링하고 문서화하는데 사용된다.
    • 주로 객체지향 방법론에서 널리 사용된다.
  • 장점
    • 표준화된 표현 : 표준화된 표기법을 사용하므로, 다양한 배경을 가진 사람들이 시스템을 이해하고 협력할 수 있다.
    • 시각화 : 복잡한 구조와 동작을 시각적으로 표현하여, 이해하기 쉽고 명확한 문서를 제공한다.
    • 추상화 : 시스템의 복잡성을 추상화하여, 주요 구성 요소와 그들 간의 관계를 강조한다. 이는 시스템 설계를 더 효과적으로 만들 수 있다.
    • 의사소통 향상 : 의사소통을 개선하고, 명확한 요구 사항을 도출할 수 있다.
    • 분석 및 설계 품질 향상 : 요구 사항을 명확히 정의하고, 시스템의 구조적 문제를 조기에 발견하여 해결할 수 있다.

'공부 > f-lab' 카테고리의 다른 글

f-lab 17주차  (0) 2024.07.09
f-lab 14주차  (0) 2024.06.18
f-lab 10주차  (0) 2024.05.23
f-lab 9주차  (0) 2024.05.19
f-lab 8주차  (0) 2024.05.07

1. 컨텍스트 스위칭이란?

https://s7won.tistory.com/11 참고

https://www.youtube.com/watch?v=Xh9Nt7y07FE&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=2 참고

 

2. CPU 스케쥴링이란? / 관련 기법

https://www.youtube.com/watch?v=LgEY4ghpTJI&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=9 참고

  • 프로세스 상태가 러닝에서 다른 상태로 바뛰면 cpu가 비어있지 않게 다음으로 실행할 프로세스를 레디큐에서 선택하는 역할
  • 디스패쳐는 이 선택된 프로세스가 cpu에서 실행될 수 있도록 하는 역할
  • 비선점 스케쥴링
    • 프로세스가 종료되거나 i/o작업으로 넘어가거나 자발적으로 다른 프로세스에게 양보하는 경우
    • 운영체제가 개입하지 않고 프로세스가 자발적으로 하는 것
    • 신사적, 협력적, 느린 응답성
  • 선점 스케쥴링
    • 비선점 상황 + 프로세스 실행이 다 안 끝나도 개입하는 상황
    • 적극적, 강제적, 빠른 응답성, 데이터 일관성 문제 발생 가능
  • 스케쥴링 알고리즘
    • first come first served : 먼저 도착한 순서대로 처리
    • shortest job first : 프로세스의 다음 씨피유 처리 시간이 가장 짧은 프로세스부터 실행
    • shortest remaining time first : 남은 씨핑 처리시간이 가장 짧은 프로세스부터 실행
    • priority : 우선순위가 높은 프로세스부터 실행
    • round robin : 타임 슬라이스로 나눠진 씨피유 타임을 번갈아가면서 실행
    • multilevel queue : 프로세스들을 그룹화해서 그룹마다 큐를 두는 방식

3. 커넥션 풀 크기가 클 때의 단점

https://www.youtube.com/watch?v=zowzVqx3MQ4&t=180s 참고

  • 메모리 사용 증가 : 많은 커넥션을 필요로 하지 않는 경우 메모리 사용량이 크게 증가
  • 커넥션 유지 비용 증가 : 각  데이터베이스 커넥션은 일정량의 시스템 리소스를 사용하는데 너무 많은 커넥션을 유지하면 CPU와 메모리 사용량이 증가하며, 이는 다른 애플리케이션이나 시스템의 성능 저하를 유발
  • 데이터베이스 과부하 : 너무 많은 커넥션이 동시에 열리면 데이터베이스 서버에 과부하를 줄 수 있다. 이는 데이터베이스 성능 저하와 연결 실패로 이어질 수 있다.

4. 커넥션 풀의 크기를 정의하는 본인만의 공식 정해오기

☝ 공식 가이드
pool size = Tn x (Cm - 1) + 1 

Tn :  쓰레드의 최대 개수
Cm :  동시에 사용하는 커넥션의 최대 개수

 

모니터링 환경 구축(서버 리소스, 서버 스레드 수, DBCP 등)
백엔드 시스템 부하 테스트 : 트래픽을 점점 늘려가면서 테스트 (nGrinder)
request per second (단위 초당 몇개의 요청까지 처리할 수 있는지), avg response time (요청에 대한 평균적인 응답 시간)
두가지를 확인
백엔드 서버, DB 서버의 cpu, 메모리 등 리소스 사용률 확인

max-connections를 먼저 확인하고 사용할 백엔드 서버 수를 고려하여 
maximunpoolsize 결정

 

5. JWT의 장단점

  • 장점
      • 토큰 검증만을 통해 사용자 정보를 확인가능 하여 추가 검증 로직이 필요 없다.
      • 매번 세션이나 데이터베이스 같은 인증 저장소가 필요 없다.
      • 사용자가 늘어나더라도 사용자 인증을 위한 추가 리소스 비용이 없다.
      • 다른 서비스에 공통 스펙으로 사용이 가능하여 확장성이 높다.
  • 단점
      • base64 인코딩 정보를 전달하기에 전달량이 많다.
      • 토큰이 탈취당할 시 만료될 때까지 대처가 불가능하다.
      • Payload부분은 누구든 디코딩하여 확인할 수 있다.

6. 리프레시 토큰이 등장한 이유

토큰은 탈취될 수도 있다는 단점이 있는데 만료시간을 짧게 설정해두면 탈취가 되더라도 그 토큰을 사용하는 데 제한이 있다. 액세스 토큰의 만료 시간을 짧게 설정해두고 서버에서 관리하는 리프레쉬 토큰의 만료 시간을 길게 설정해두고 사용하면 이러한 단점을 해결할 수 있다.

 

7. 왜 세션/쿠키 인증 방식 대신에 JWT토큰을 썼나요?

  • 서버에 상태를 저장할 필요가 없기 때문에 별도의 서버를 더 추가하지 않아도 돼서 확장하기에 좋다.
  • 사용자의 인증 정보가 토큰 자체에 있기 때문에 다른 데이터를 서버에 요청할 필요가 없기 때문에 성능이 향상된다.
  • 자체적인 만료 시간을 가지고 있어 만료에 대한 관리가 편리하다.

8. DispatcherServlet이란?

  • 서블릿 컨테이너의 가장 앞단에서 HTTP 프로토콜로 들어오는 모든 요청을 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러
  • 프론트 컨트롤러 : 서블릿 컨테이너의 제일 앞에서 서버로 들어오는 클라이언트의 모든 요청을 받아서 처리해주는 컨트롤러, 공통의 로직에 대한 처리 가능
  • DispatcherServlet의 흐름
    1. 클라이언트에서 요청이 오면 DispatcherServlet이 요청을 받는다.
    2. HandlerMapping을 통해 요청에 맞는 컨트롤러를 찾아낸다.
    3. 찾아낸 컨트롤러를 Handler Adapter를 통해 해당 컨트롤러의 메서드를 실행시킨다.
    4. 컨트롤러는 요청을 처리한 뒤 해당 요청에 대한 결과와 뷰에 대한 정보를 다시 DispatcherServlet에 전달한다.
    5. 받은 정보로 DispatcherServlet 은 View Resolver를 뷰 파일을 찾는다.

 

9. JSP/서블릿 서버 기준으로 요청이 들어오고 응답이 나가는 과정 설명

  1. 클라이언트 요청: url 요청
  2. 웹 서버: 요청 수신 후 서블릿 컨테이너로 전달
  3. 서블릿 컨테이너: /myapp/hello.jsp 요청을 수신
  4. JSP 컴파일 과정: hello.jsp가 서블릿으로 변환되고 컴파일됨
  5. 요청 매핑: /myapp/hello.jsp 요청을 컴파일된 서블릿에 매핑
  6. 서블릿 초기화: 서블릿이 초기화되지 않은 경우 init() 호출
  7. 요청 처리: 서블릿의 doGet() 또는 doPost() 메서드 호출
  8. 비즈니스 로직: 데이터베이스 조회, 연산 등 수행
  9. 응답 생성: 서블릿이 HTML, JSON 등을 생성
  10. 응답 반환: 클라이언트에 응답 전송( HttpServletResponse 객체 이용)
  11. 클라이언트 수신: 브라우저가 응답을 렌더링하여 사용자에게 표시

'공부 > f-lab' 카테고리의 다른 글

f-lab 14주차  (0) 2024.06.18
f-lab 11주차  (0) 2024.05.28
f-lab 9주차  (0) 2024.05.19
f-lab 8주차  (0) 2024.05.07
f-lab 7주차  (0) 2024.04.30

+ Recent posts