728x90
반응형
    const [windowWidth, setWindowWidth] = useState(0);

    const resizeWindow = () => {
        setWindowWidth(window.innerWidth)
    }

    useEffect(() => {
        setWindowWidth(window.innerWidth)
        window.addEventListener("resize", resizeWindow)
        return () => {
            window.removeEventListener("resize", resizeWindow)
        }
    }, [windowWidth])

 

상태를 만들고 window 사이즈를 측정해서 쉽게 만들 수 있다.

728x90
반응형
728x90
반응형

1. 인스턴스 서버에 도커 설치

$ sudo wget -qO- http://get.docker.com/ | sh

 

2. 인스턴스 서버에서 도커 로그인

$ docker login

 

3. 도커 시작

$ sudo systemctl start docker

 

4. 도커허브에서 이미지 pull 

ex) Next.js

$ docker pull 도커허브아이디/web_client:버전정보

 

ex) Spring

$ docker pull 도커허브아이디/java_server:버전정보

 

5. 이미지 확인

$ docker images

 

 

6. Docker run

포트는 프로젝트에 맞게 설정해야함.

$ docker run -p 8083:8083 -d --rm --name java_server ID/Repository

 

$ docker run -p 80:3000 -d --rm --name web_client ID/Repository

 

728x90
반응형
728x90
반응형

1. env 수정

 

ex) /next.config.js

/** @type {import('next').NextConfig} */
const path = require('path');
const withImages = require('next-images');

module.exports = {
  reactStrictMode: true,
  async rewrites() {
    if (process.env.NODE_ENV === "production") {
      return [
        {
          source: process.env.PRODUCTION_JAVA_SERVER_PATH,
          destination: process.env.PRODUCTION_JAVA_SERVER_URL,
        }

      ];
    } else {
      return [
        {
          source: process.env.JAVA_SERVER_PATH,
          destination: process.env.JAVA_SERVER_URL,
        }
      ];
    }
  },
};

 

 

/.env.local

NODE_ENV = 'development'

PRODUCTION_JAVA_SERVER_PATH = '/java/:path*'
PRODUCTION_JAVA_SERVER_URL = 'http://사용하는 IP 주소:8083/:path*'

JAVA_SERVER_PATH = '/java/:path*'
JAVA_SERVER_URL = 'http://localhost:8083/:path*'

 

2. Dockerfile 생성

프로젝트 루트 경로에 만들어준다.

/Dockerfile

# 위에서 도커 허브 node 이미지를 기반으로 로컬로 다운로드 및 캐싱 되었기 때문에 이미지를 가져올 수 있다.
FROM node:18.4.0

# 만약 컨테이너 안의 이미지의 경로가 /app 이런식으로 되어있다면 작업할 div 경로를 설정할 수도 있다.
# 설정해주면 COPY 의 두번째 경로를 ./ 이것으로 했을 때 자동으로 /app 경로가 된다.
WORKDIR /app

# package.json 파일을 복사한다. 만약 다시 빌드할 때 변경사항이 없을 경우 npm install까지 그냥 넘어간다.
COPY package.json /app

# 이미지를 받으면 npm install을 자동으로 해줌
RUN npm install


# 어떤 파일이 이미지에 들어가야 하는지 
# 첫 번째 .은 이 프로젝트의 모든 폴더 및 파일들 (Dockerfile을 제외한)
# 두 번째 .은 파일을 저장할 컨테이너 내부 경로 (ex /app)
COPY . /app

# 배포환경으로 설정
ENV NODE_ENV=production

RUN npm run build

# 도케에게 우리가 서버를 실행할 포트를 말해준다.
EXPOSE 3000

# 이미지가 생성될 때 실행되지 않고 컨테이너가 실행될 때 수행하는 명령어
CMD ["npm","start"]

 

당연한 말이지만 개발 서버와 node 버전을 맞춰주어야 한다. 

 

3. dockerignore 생성

/.dockerignore

.node_modules

.next

 

4. next.config.js 수정

module.exports = {
  output: 'standalone'
}

 

5. 이미지 빌드

$ docker build -t 도커허브아이디/web-client:버전정보 .

web_client 는 임의로 제가 지은 이름입니다.

docker 프로그램에서 확인할 수도 있다.

 

혹시 맥북에서 빌드하고 linux/amd64 서버에 배포할 예정이라면 

docker buildx build --platform=linux/amd64 -t 도커허브아이디/web_client:버전정보 .

 

6. Dockerhub Repository 생성

 

7. 도커허브 업로드

# 업로드
docker push 도커허브아이디/web_client

 

 


Server

 

1. 인스턴스 서버에 도커 설치

$ sudo wget -qO- http://get.docker.com/ | sh

 

2. 인스턴스 서버에서 도커 로그인

$ docker login

 

3. 도커 시작

$ sudo systemctl start docker

 

4. 도커허브에서 이미지 pull 

$ docker pull 도커허브아이디/web_client:버전정보

 

5. 이미지 id 확인

$ docker images

 

6. 컨테이너 실행

$ docker run -p 80:3000 -d --rm 도커허브아이디/web_client
728x90
반응형
728x90
반응형

1. Dockerfile 작성

프로젝트 루트경로에 작성해준다.

FROM openjdk:11
# FROM amazoncorretto:11 ==> amazon corretto 11 사용할 경우
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
# ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","/app.jar"]
# => 설정파일을 분리해서 사용할 때
# java -jar -Dspring.profiles.active=prod app.jar

 

2. Build

# Spring Boot 빌드
./gradlew build -x test

 

3. 이미지 생성

# gradle linux/amd64 옵션은 맥북 M1을 위한 옵션
$ docker build --build-arg DEPENDENCY=build/dependency -t 도커허브 ID/Repository --platform linux/amd64 .

# maven
$ docker build -t 도커허브 ID/Repository --platform linux/amd64 .

# 확인
$ docker images

 

Spring Boot 2.3.x 버전 이상인 경우 Dockerfile 작성 없이 Plugin으로 이미지 생성이 가능하다.

# yml, properties를 여러개 사용하는 경우 profile을 지정하여 image를 생성한다.
$ ./gradlew bootBuildImage --imageName=ID/Repository

4. Docker 업로드

# 로그인
$ docker login

# 업로드
$ docker push ID/Repository

 

배포 팁

환경변수중에 SPRING_PROFILES_ACTIVE=prod 를 지정하면 자동으로 application-prod.properties 파일을 바라봅니다.

개발할 땐 application.properties 설정을 바라보고 docker로 운영 서버에 배포할 땐 -e SPRING_PROFILES_ACTIVE=prod 만 추가하면 됩니다.

728x90
반응형
728x90
반응형

1. 의존성 추가

dependencies {
   runtimeOnly 'mysql:mysql-connector-java'
}

 

2. application properties

#Database
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.profiles.active=local
spring.datasource.url= jdbc:mysql://db-d618t-kr.vpc-pub-cdb.ntruss.com:3306/pay_calculator_test?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=nomu9032
spring.datasource.password=jmk1994**
spring.datasource.hikari.connection-timeout= 3000
spring.datasource.hikari.validation-timeout= 3000
spring.datasource.hikari.minimum-idle= 5
spring.datasource.hikari.max-lifetime= 240000
spring.datasource.hikari.maximum-pool-size= 20

# Hibernate setting
spring.jpa.database= mysql
spring.jpa.database-platform= org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=validate
728x90
반응형
728x90
반응형

1. 버킷 생성

https://typo.tistory.com/entry/Nodejs-Multer-S3-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-AWS-S3-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84-1

 

Node.js | Multer 를 이용한 AWS S3 파일 업로드 구현(1)

1. aws에 접속하여 IAM 사용자를 생성한다 2. 권한으로 AmazonS3FullAccess를 할당해준다. 3. 해당 Access key ID, Secret access key를 알고 있어야 한다. 4. aws s3 화면에 접속해서 버킷을 만들어준다. 5...

typo.tistory.com

 

2. build.gradle 의존성 추가

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

 

3. application.properties 추가

중요한 내용을 담기 때문에 .gitignore에 설정 파일을 추가해준다.

 

4. S3Config 

backend/aws/S3Config

package mobile.backend.aws;

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 amazonS3Client() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

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

 

5. WebConfig

Config 파일을 한 곳에서 관리한다면 Class를 추가해준다.

package mobile.backend;

...

@Configuration
@Import({AopConfig.class, RedisConfig.class, S3Config.class})
public class WebConfig implements WebMvcConfigurer {

...

 

6. S3Uploader

backend/aws/S3Uploader

package mobile.backend.aws;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor    // final 멤버변수가 있으면 생성자 항목에 포함시킴
@Service
public class S3Uploader {
    private final AmazonS3Client amazonS3Client;

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

    // MultipartFile을 전달받아 File로 전환한 후 S3에 업로드
    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        File uploadFile = convert(multipartFile)
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
        return upload(uploadFile, dirName);
    }


    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + uploadFile.getName();
        String uploadImageUrl = putS3(uploadFile, fileName);

        removeNewFile(uploadFile);  // 로컬에 생성된 File 삭제 (MultipartFile -> File 전환 하며 로컬에 파일 생성됨)

        return uploadImageUrl;      // 업로드된 파일의 S3 URL 주소 반환
    }

    // 1. 로컬에 파일생성
    private Optional<File> convert(MultipartFile file) throws  IOException {
        File convertFile = new File(file.getOriginalFilename());
        if(convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    }

    // 2. S3에 파일업로드
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(
                new PutObjectRequest(bucket, fileName, uploadFile)
                        .withCannedAcl(CannedAccessControlList.PublicRead)	// PublicRead 권한으로 업로드 됨
        );
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    // 3. 로컬에 생성된 파일삭제
    private void removeNewFile(File targetFile) {
        if(targetFile.delete()) {
            log.info("파일이 삭제되었습니다.");
        }else {
            log.info("파일이 삭제되지 못했습니다.");
        }
    }

    // s3 파일 삭제
    public void delete(String fileName) {
        log.info("File Delete : " + fileName);
        amazonS3Client.deleteObject(bucket, fileName);
    }
}

 

7. Controller

클라이언트(React) 에서 보낸 데이터입니다.

const config = {
    headers: {
        "content-type": "multipart/form-data",
    },
};

const formData = new FormData();

formData.append("file_object", fileObject);

await axios.post('/java/file/upload', formData, config)

 

backend/module/file/FileController

...

    @PostMapping("/upload")
    public CommonResponse<String> upload(
            @RequestParam("file_object") MultipartFile fileObject
    ) throws IOException {
        log.info("multipartFile={}", fileObject);
        return new CommonResponse<String>(
                true,
                service.upload(
                        fileObject
                ));
    }
    
...

 

8. Service

backend/module/file/FileService

...

	@Autowired
    private S3Uploader s3Uploader;

...

public String upload( MultipartFile fileObject ) throws IOException {

        if(fileObject.isEmpty()) {
        }
        else {
            String storedFileName = s3Uploader.upload(fileObject,"images");
            log.info("fileName={}", storedFileName);
        }
        return "ok";
    }

 

9. main 함수 파일에 다음을 추가해준다.

@SpringBootApplication
public class BackendApplication {

	static {
		System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true");
	}

	public static void main(String[] args) {
		SpringApplication.run(BackendApplication.class, args);
	}

}

 

서버를 키고 파일 저장을 시도해보면 아래와 같이 성공한 것을 볼 수 있다.

728x90
반응형
728x90
반응형

아래 옵션으로 편리하게 request 데이터를 확인할 수 있다.

 logging.level.org.apache.coyote.http11=debug

 

package hello.upload.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {

    @Value("${file.dir}")
    private String fileDir;

    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
    }

    @PostMapping("/upload")
    public String saveFile(@RequestParam String itemName,
                           @RequestParam MultipartFile file, HttpServletRequest
                                   request) throws IOException {
        log.info("request={}", request);
        log.info("itemName={}", itemName);
        log.info("multipartFile={}", file);
        if (!file.isEmpty()) {
            String fullPath = fileDir + file.getOriginalFilename();
            log.info("파일 저장 fullPath={}", fullPath);
            file.transferTo(new File(fullPath));
        }
        return "upload-form";
    }

}
728x90
반응형
728x90
반응형

1. 의존성 추가

// jwt
	implementation 'io.jsonwebtoken:jjwt:0.9.1'

 

2. JwtTokenManager 

backend/auth/JwtTokenManager

package web.backend.auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenManager {

    private static final String secret = "secretKey!!!";

    // 토큰 유효 기간
    public static final long JWT_TOKEN_VALIDITY = 60 * 60 * 24 * 1000L; //하루

    /**
     *  토큰 생성
     */
    public String generateToken(String id, Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)  // 정보 저장
                .setId(id)
                .setIssuedAt(new Date(System.currentTimeMillis()))  // 토큰 발행 시간 정보
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY)) // set Expire Time
                .signWith(SignatureAlgorithm.HS512, secret)// 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    /**
     *  토큰 id 반환
     */
    public String getTokenIdFromToken(String token) {
        return getClaimFromToken(token, Claims::getId);
    }

    /**
     *  토큰이 만료되었는지 Boolean 반환
     */
    public Boolean isTokenExpired(String token) {
        final Date expiration = getClaimFromToken(token, Claims::getExpiration);
        return expiration.before(new Date());
    }

    /**
     *  토큰 자체에 대한 정보 추출(id, expire 등)
     */
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token).getBody();// JWT payload 에 저장되는 정보단위
        return claimsResolver.apply(claims);
    }

    /**
     *  토큰 안의 모든 정보 추출
     */
    public Claims getClaimsFromToken(String token) {
        final Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token).getBody();// JWT payload 에 저장되는 정보단위
        return claims;
    }

}

 

3. Test

test/java/web/backend/auth/JwtTest

package web.backend.auth;

import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@Slf4j
public class JwtTest {

    @Autowired
    JwtTokenManager jwtTokenProvider = new JwtTokenManager();

    Map<String, Object> map = new HashMap<>();

    @BeforeEach
    public void before() {
        map.put("userId","teepo");
    }

    @Test
    public void test() {

        // 토큰 생성
        String tokenString = jwtTokenProvider.generateToken("1", map);
        log.info("tokenString={}",tokenString);
        assertNotNull(tokenString);

        // 토큰 Id
        String tokenId = jwtTokenProvider.getTokenIdFromToken(tokenString);
        log.info("tokenId={}", tokenId);
        assertEquals("1",tokenId);

        // 토큰이 만료되었는지
        Boolean tokenExpired = jwtTokenProvider.isTokenExpired(tokenString);
        log.info("tokenExpired={}",tokenExpired);
        assertEquals(false, tokenExpired);

        // 토큰 안의 모든 정보
        Claims tokenClaims = jwtTokenProvider.getClaimsFromToken(tokenString);
        log.info("tokenClaims={}", tokenClaims);

        // before 메소드에서 생성한 데이터 조회
        String value = tokenClaims.get("userId").toString();
        log.info("userId={}",value);
        assertEquals("teepo", value);

    }
}

 

4. 로그 확인

 


 

이번엔 클라이언트한테 요청을 받고, 쿠키 안에 Token을 넣은 뒤 확인해보자. 코드는 전 포스트에서 이어진다.

 

1. Controller

backend/module/user/UserController

    @PostMapping("/test")
    public CommonResponse<String> jwtGenerateTest(HttpServletResponse response, @RequestBody User user) {
        return new CommonResponse<String>(true, userService.jwtTest(response,user));
    }

 

2. Service

backend/module/user/UserService

    JwtTokenManager jwtTokenManager = new JwtTokenManager();

    public String jwtTest(HttpServletResponse response ,User user) {

        Map<String, Object> tokenMap = new HashMap<>();

        tokenMap.put("userId", user.getUserId());

        String tokenString = jwtTokenManager.generateToken("token1", tokenMap);

        log.info("tokenValue={}",jwtTokenManager.getClaimsFromToken(tokenString));


        Cookie cookie = new Cookie("token1",tokenString);
        cookie.setMaxAge(86400000); // 하루

        response.addCookie(cookie);

        return "ok";
    }

 

3. Postman

하단에 Cookies 버튼을 누르면 확인할 수 있다.

728x90
반응형
728x90
반응형

개요

  Next.js 서버 기존 Node.js 서버 마이그레이션 Java 서버 DB
State Camel      
Request Snake Snake Snake  
Response Snake Snake Snake  
Model   Snake Camel  
Column       Snake

 

Node.js 서버에서는 Request를 있는 그대로 받아서 사용하였기에 별 상관이 없었지만

Java 서버에서는 Dto 객체를 만들고 Dto를 토대로 Entity에 대한 Query를 해야 하기 때문에 결국 Camel을 한 번 거쳐서 가야 했다.

 

 

문제점

결론부터 얘기하자면 

'insurance_JikJongCd' <-- 이런 애들이 문제다

 

그 당시에는 외부 api를 쓰면서 해당 Request 형식에 맞춰 이렇게 만들었었고 Node.js 에서 사용할 때는 문제가 없었지만

Spring에서 Entity를 사용하면서 문제가 발생했다. 

 

아래와 같이 테이블에 설정해주면 Snake 표기법으로 된 Request Camel 표기법으로 된 엔티티 필드와 매칭시킬 수 있다.

또한 Response 또한 snake case 로 반환할 수 있다.

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User {

 

 

또한 Spring은 똑똑해서 별다른 설정을 해주지 않아도 Camel 표기법으로 된 엔티티 필드 Snake 표기법으로 된 Db Column이랑 매칭을 시켜준다.

 

// Entity
@Column(name="user_name")
private String userName;

 

 

근데 'insurance_JikJongCd' <-- 이런 애들이 문제다

 

Request로 받을 경우 insurance_Jik_jong_cd  이런 식으로 읽기 때문에 인식을 하지 못한다. 

 

 

JsonProperty Annotation을 쓰면 Response를 Snake 표기법으로 보낼 수도 있다.

// Entity
@Column(name="user_name")
@JsonProperty(value="user_name")
private String userName;

// Response
user_name : "teepo"

 

 

참고1

PropertyNamingStrategies 구성 요소들이 Deprecated 되면서 application.properties에서 다음과 같이 Request, Response를

Snake 표기법으로 통신할수 있다.

spring.jackson.property-naming-strategy = SNAKE_CASE

 

이 방법으로 @JsonProperty, @JsonNaming 어노테이션을 사용하지 않아도 된다.

 

 

 

참고

appication.properties 에서 지원해주는 카멜표기법을 사용하지않고 사용자 임의로 설정하는 것이 있다.

#spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

 

결론은 섞어서 쓰지 말고 설계를 잘하고 스프링이 편하라고 제공해준 것을 잘 쓰자.

728x90
반응형
728x90
반응형

1. Redis docker image pull

$ docker pull redis # redis 이미지 받기
$ docker images # redis 이미지 확인
$ docker run -p 6379:6379 --name some-redis -d redis # redis 시작하기
$ docker ps # redis 실행 확인

 

 

2. 의존성 추가

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

 

 

3. application.properties

#Redis
spring.redis.host= localhost
spring.redis.port= 6379

 

 

Redis Cache 활성화를 위한 @Annotation

  • @EnableCaching
    • SpringBoot에게 캐싱 기능이 필요하다고 전달
    • SpringBoot Starter class에 적용
  • @Cacheable
    • DB에서 애플리케이션으로 데이터를 가져오고 Cache에 저장하는 데 사용
    • DB에서 데이터를 가져오는 메서드에 적용
  • @CachePut
    • DB의 데이터 업데이트가 있을 때 Redis Cache에 데이터를 업데이트
    • DB에서 PUT/PATCH와 같은 업데이트에서 사용
  • @CacheEvict
    • DB의 데이터 삭제가 있을 때 Redis Cache에 데이터를 삭제
    • DB에서 DELETE와 같은 삭제에서 사용

 

4. RedisConfig 파일 생성

/backend/redis/RedisConfig

package web.backend.redis;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public CacheManager testCacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofMinutes(3L));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
    }
}

매니저에 도메인을 Serialize 하게끔 설정해두었기에 도메인에 따로 Serializable를 import 안해줘도 된다.

다만 어노테이션에서 매니저를 호출해야 한다.

 

backend/WebConfig

@Import({AopConfig.class, RedisConfig.class})

 

5. LocalDateTime in Domain

Serialize를 위해 LocalDateTime 타입 필드는 설정을 따로 해줘야 한다.

backend/module/user/User

...

    @CreatedDate
    @Column(name="user_createdat", updatable = false)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime user_creadtedat;

    @LastModifiedDate
    @Column(name="user_updatedat")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime user_updatedat;
    
...

 

6. Service 적용

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {

    private final UserQueryRepository userQueryRepository;
    private final UserSpringJpaRepository userSpringJPARepository;

    @Cacheable(value = "User", cacheManager = "testCacheManager")
    public List<User> findAll() {
        return userSpringJPARepository.findAll();
    }

    @Cacheable(value = "User", key = "#id", cacheManager = "testCacheManager")
    public User findByIndexId(Long id) {
        return userSpringJPARepository.findById(id).get();
    }

    public String save(User user) {
        userSpringJPARepository.save(user);
        return "ok";
    }

    @CachePut(value = "Order", key = "#id", cacheManager = "testCacheManager")
    public String update(Long id, User user) {
        User userOne = userSpringJPARepository.findById(id).get();
        userOne.changeUser(user.getUserId());
        return "ok";
    }

    @CacheEvict(value = "Order", key = "#id", cacheManager = "testCacheManager")
    public String delete(Long id) {
        userSpringJPARepository.deleteById(id);
        return "ok";
    }


}

 


테스트

1. 데이터 추가

테스트를 위해 WebConfig에 어플리케이션 빌드 시 데이터를 추가하는 로직을 작성해준다.

package web.backend;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.aop.AopConfig;
import web.backend.interceptor.LogInterceptor;
import web.backend.module.user.User;
import web.backend.module.user.repository.UserSpringJpaRepository;
import web.backend.redis.RedisConfig;

@Configuration
@Import({AopConfig.class, RedisConfig.class})
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico"
                        , "/error", "/error-page/**" //오류 페이지 경로
                );
    }

    @Autowired
    UserSpringJpaRepository userSpringJpaRepository;

    @Bean
    public void addUsers() {
        for(int i = 0; i < 100; i++) {
            User user = new User();
            user.changeUser("member" + i);
            userSpringJpaRepository.save(user);
        }
    }

}

 

 

2. Redis Docker Container

Spring Server를 키고 Redis Container에 접속한 후 요청을 보내서 로그를 확인해보자.

$ docker ps    
$ docker exec -it some-redis /bin/bash
$ redis-cli monitor

 

postman으로 요청을 보내보자.

그러면 아래와 같이 캐시가 저장된 것을 확인할 수 있다.

 

postman에도 데이터가 잘 도착했다.

이번엔  key를 지정하고 다시 보내보자. ( id를 파라미터로 보내는 로직 )

 

역시 정상적으로 동작이 잘 되었다.

 

 

3. 성능 확인

 

- 처음 캐시가 사용되기 전 요청

 

- 두번 째 요청

 

 


다만,

아래와 같은 점을 고려해야 한다.

  1. 리스트를 처음에 캐싱한다.
  2. 리스트의 어떤 항목이 생성되거나 수정되거나 삭제된다.
  3. 캐시된 리스트의 데이터가 바뀌어야 하지만 그렇지 못한다.

캐시에 변경사항이 생길 경우 다른 캐시에 영향이 끼친다면 로직을 따로 만들어 주어야 한다. 당장에라도 적용하고 싶으면

findAll()의 @Cacheable 어노테이션은 제거한 상태로 쓰고 나중에 각 테이블의 연관관계를 고려해서 로직을 구성해주자.

728x90
반응형

+ Recent posts