AOP란?

  • Spring에서 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 프로그래밍에서 반복되는 코드를 모듈화하는 프로그래밍 패러다임입니다.
    이는 객체 지향 프로그래밍(OOP)을 보완하는 방식으로, 공통의 관심사(Cross-cutting Concerns)를 한 곳에 모아 관리합니다.
    AOP는 주로 로깅, 보안, 트랜잭션 관리와 같은 기능에서 사용됩니다.

  • AOP의 핵심 개념은 'Aspect'로 여러 객체에 공통으로 적용되는 기능을 의미합니다.
    예를 들어, 로깅은 애플리케이션의 많은 부분에서 필요하지만 각각의 객체나 메소드에 로깅 코드를 직접 삽입하는 것은 비효율적입니다.
    AOP를 사용하면, 공통 기능을 별도의 Aspect로 정의하고, 필요한 객체나 메소드에 독립적으로 적용할 수 있습니다.

AOP 개념 용어

  1. Aspect(관점)
  • AOP의 핵심 요소로, 흩어진 관심사를 모듈화한 것입니다.
  • 예를 들어 로깅, 트랜잭션 관리 또는 보안과 같은 기능을 Aspect로 정의할 수 있습니다.
  1. Join Point
  • Advice가 적용될 수 있는 위치, 예를 들어 메소드 실행, 객체 생성 등 프로그램 실행 중 특정 지점을 의미합니다.
  1. Advice
  • 실제로 Aspect가 어떤 일을 수행되는지 작업(로직)을 정의한 것입니다.
  • @Before: Join Point 전에 실행됩니다.
  • @After Returning: Join Point가 정상적으로 완료된 후에 실행됩니다.
  • @After Throwing: 메소드 실행 중 예외가 발생했을 때 실행됩니다.
  • @After: Joint Point가 실행된 후, 예외 발생 여부에 상관없이 실행됩니다.
  • @Arround: Joint Point 전후로 실행되며, 메소드 실행을 감싸는 형태입니다.
  1. Pointcut
  • Join Points 중에서 실제로 Advice가 적용될 위치를 선별하는 표현식입니다.
  • Pointcut을 통해 특정 메소드나 클래스의 메소드들에 대한 Advice 적용 여부를 정의할 수 있습니다.

@Pointcut

  • @Pointcut 은 Advice가 적용될 메소드나 클래스의 집합을 정의하는 데 사용되는 어노테이션입니다.
  • 쉽게 말해, @Pointcut은 어떤 메소드에 Advice를 적용할지 결정하는 "필터"와 같은 역할을 합니다.

@Pointcut을 사용하지 않은 경우

@Component
@Aspect
@Slf4j
public class MyAspect{
    // Before Advice: com.example 패키지 내 모든 메소드 실행 전에 적용
    @Before("execution(* com.example..*(..))")
    public void beforeAdvice(){
        log.info("Before method execution");
    }

    // After Advice: com.example 패키지 내 모든 메소드 실행 후에 적용
    @After("execution(* com.example..*(..))")
    public void afterAdvice(){
        log.info("After method execution");
    }
}

@Pointcut을 사용한 경우

@Component
@Aspect
@Slf4j
public class MyAspect{
    // Pointcut 정의: com.eample 패키지 내 모든 메소드
    @Pointcut("exectuion(* com.example..*(..))")
    public void myPointcut() {}

    // Before Advice: myPointcut에 정의된 메소드 실행 전에 적용
    @Before("myPointcut()")
    public void beforeAdvice(){
        log.info("Before method execution");
    }

    // After Advice: myPointcut에 정의된 메소드 실행 후에 적용
    @After("myPointcut()")
    public void afterAdvice(){
        log.info("After method execution");
    }
}

@Pointcut을 사용한 경우 이점

  1. 재사용성
  • @Pointcut을 사용하는 경우, @Pointcut을 한 곳에서 정의하고 여러 Advice에서 재사용 할 수 있어 코드의 중복을 줄일 수 있습니다.
  1. 유지보수
  • @Pointcut 을 사용하면, 추후 Pointcut을 변경할 때 한 곳에서만 수정하면 됩니다.
  1. 가독성
  • @Pointcut 을 사용하면, Pointcut의 목적을 이름으로 명확히 할 수 있어 가독성이 좋아집니다.

@Pointcut 사용 방법

execution

execution([수식어][리턴타입][클래스이름][이름]([파라미터])

  • 수식어: public, private 등 수식어를 명시합니다. (생략 가능)
  • 리턴타입: 리턴 타입을 명시합니다.
  • 클래스이름 및 이름: 클래스이름과 메서드 이름을 명시합니다. (클래스 이름은 풀 패키지명으로 명시해야 합니다. 생략 가능)
  • 파라미터: 메서드의 파라미터를 명시합니다.
  • "*": 모든 값을 표현합니다.
  • "..": 0개 이상을 의미합니다.

ex)
execution(public Integer com.example.aop..(*))
-> com.example.aop 패키지의 하위 클래스 중 public 접근자면서 반환타입이 Integer, 한개의 파라미터를 가지는 메소드

execution(com.example...get*(..))
-> com.example 패키지 및 하위 패키지의 클래스 중 이름이 get으로 시작하며 파라미터 수나 타입에 관계없이 모든 메서드

execution(com.example.aop..Service.*(..))
-> com.example.aop 패키지 및 하위 패키지에 속해있고 이름이 Service로 끝나는 파라미터 수나 타입에 관계없이 모든 메서드

execution(com.example.aop.BoardService.(.))
-> com.example.aop 패키지의 BoardService 클래스의 파라미터 수나 타입에 관계없이 모든 메서드

execution(some(,))
-> 메서드 이름이 some으로 시작하고 파라미터가 2개인 모든 메서드

within 명시자

ex)
within(com.example.aop.SomeService)
-> com.example.aop.SomeService 인터페이스의 모든 메서드

within(com.example.aop.*)
-> com.example.aop 패키지의 모든 메서드

within(com.example.aop..*)
-> com.example.aop 패키지 및 하위 패키지의 모든 메서드

bean 명시자

ex)
bean(someBean)
-> 이름이 someBean인 빈의 모든 메서드

bean(some*)
-> 빈의 이름이 some으로 시작하는 빈의 모든 메서드



Advice 사용 방법

  1. @Around
  • 핵심 메서드와 공통 메서드의 실행 시점을 자유롭게 설정할 수 있는
  • 어드바이스를 활용하면 메소드의 실행을 둘러싸 여러 가지 추가 기능을 구현할 수 있으며, 비즈니스 로직에 영향을 주지 않으면서 중요한 관심사를 관리할 수 있습니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerformanceAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void monitor() { }

    // Around 어드바이스 정의
    @Around("monitor()")
    public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        try {
            // 대상 메소드 실행
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long elapsedTime = System.currentTimeMillis() - startTime;
            System.out.println(joinPoint.getSignature().getName() + " 실행 시간: " + elapsedTime + "ms");
        }
    }
}
  1. @AfterThrowing
  • 메소드 실행 중 예외가 발생한 후에 실행됩니다.
  • 발생한 예외에 대한 정보에 접근할 수 있습니다.
  • 발생한 예외에 따라 적절한 처리 로직을 수행할 수 있습니다.
  • 예외 상황에 대한 로그 기록, 리소스 정리, 사용자에게 오류 정보 전달 등 다양한 예외 처리 로직을 구현하는 데 유용하게 사용됩니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ErrorHandlingAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() { }

    // AfterThrowing 어드바이스 정의
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logAfterThrowingAllMethods(Exception ex) throws Throwable {
        System.out.println("Exception caught in AfterThrowing advice: " + ex.getMessage());
    }
}
  1. @AfterReturning
  • 메소드가 성공적으로 실행되고 결과를 반환한 후에 실행됩니다.
  • 대상 메소드의 반환값에 접근하여 처리할 수 있습니다.
  • 메소드 실행의 성공 여부를 로깅하는데 사용될 수 있습니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() { }

    // AfterReturning 어드바이스 정의
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("Method returned value is : " + result);
    }
}
  1. @Before
  • 대상 메소드 실행 전에 특정 로직을 수행합니다.
  • 메소드가 실행되기 전 필요한 파라미터 검증이나 초기 설정을 수행할 수 있습니다.
  • 메소드 실행 전 정보를 로그로 남길 수 있습니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() { }

    // Before 어드바이스 정의
    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before executing method: " + joinPoint.getSignature().getName());
    }
}
반응형

'개발 > Spring' 카테고리의 다른 글

응집도와 결합도란?  (1) 2023.12.26
[Querydsl] DTO 조회 방법  (0) 2023.12.17
OAuth2란? OAuth2 예시  (1) 2023.12.15
DTO <-> Entity 변환 이유!  (0) 2023.12.14

Docker File 이란?

  • Docker 컨테이너를 생성하기 위한 구성 파일입니다.
  • Dokcer 이미지를 만들기 위한 명령어와 설정을 포함하고 있습니다.
  • Docker File을 사용하면 애플리케이션과 종속성을 코드 형태로 정의할 수 있으며, 이를 통해 일관된 환경에서 애플리케이션을 배포하고 실행할 수 있습니다.

Docker File 기본적인 구성

FROM #운영체제 이미지
RUN #실행할 명령어
CMD #컨테이너 명령 실행

DockerFile 문법

명령어 설명 명령어 설명
FROM 베이스 이미지 지정 RUN 명령어 실행
CMD 컨테이너 실행 명령 EXPOSE 포트 노출
ENV 환경 변수 ADD 파일 / 디렉토리 추가
COPY 파일 복사 ENTRYPOINT 컨테이너 실행 명령
VOLUME 볼륨 마운트 USER 사용자 지정
WORKDIR 작업 디렉토리 ARG Dockerfile 안의 변수
ONBUILD 빌드 완료 후 실행되는 명령 LABEL 라벨 설정
STOPSIGNAL 종료 시그널 설정 HEALTHCHECK 컨테이너 상태 체크
SHELL 기본 쉘 설정
  • 대다수의 Dockerfile을 보면 FROM 부터 시작합니다.
  • 운영체제를 지정하는데 메모리 기반으로 돌아가기 때문에 용량을 적게 설정하는 것이 필요합니다.

명령어 상세 설명

1. RUN

  • 이미지 생성과정에서 애플리케이션 / 미들 웨어 설치, 환경 설정을 위한 명령 등을 정의합니다.

1) Shell 형식

  • 쉘 프롬프트에 명령을 기술하는 방식입니다.
  • 베이스 이미지 위에서 /bin/sh -c를 사용해서 명령을 수행하는 것과 동일합니다.

RUN apt-get update && apt-get install -y nginx

2) exec 형식

  • 쉘을 경유하지 않습니다.
  • 직접 실행이 아니라 $HOME 같은 쉘 환경 변수를 사용할 수 없습니다.
  • 명령어가 단독적으로 실행되는 것이 아니기 때문에 JSON 배열 형식으로 정의합니다.

RUN ["/bin/bash", "apt-get install -y nginx" ]

2. CMD

  • 생성된 이미지를 바탕으로 컨테이너 내부에서 수행될 작업이나 명령을 실행합니다.
  • Dockerfile에 하나의 CMD 명령만 기술 가능하기 때문에, 여러개를 기록한다면 마지막 명령만 유효합니다.

ex) Nginx 이미지를 생성할 때 Nginx를 설치하는 것은 RUN 명령이지만, Nginx Daemon을 실행하는 것은 CMD 명령어 입니다.

  • shell, exec 형식을 지원합니다.
#shell
CMD nginx -g 'daemon o ff;'

#exec
CMD ["nginx", "-g", "daemon o ff;" ]

3. ONBUILD

  • 해당 이미지를 import 하는 곳에서 사용하기 위한 명령어 입니다.
  • Dockerfile 에 ONBUILD 명령을 사용하여 어떤 명령을 실행하도록 설정하여 빌드하고 이미지를 작성하게 되면, 그 이미지를 다른 Dockerfile 에서 베이스 이미지로 설정하여 빌드했을 때 ONBUILD 명령에서 지정한 명령이 실행됩니다.

4. ENV, ARG

  • 둘 모두 환경 변수를 지정해 줍니다.
  • ENV는 컨테이너 내부에서 사용하는 환경 변수, ARG는 빌드 되는 과정에서만 사용하는 환경 변수 입니다.
FROM nginx
ENV FOOD "Chicken" \
Summer Cola Sea Vacation \
ENV Time 1116
CMD ["/bin/bash"]

5. EXPOSE

  • 컨테이너의 공개포트를 지정하고 컨테이너가 대기하고 있는 포트를 알려줍니다.
  • docker run -p 옵션을 통해 호스트의 포트 번호와 매핑할 수 있습니다.
    예를 들면 -p 8080:443처럼 사용할 수 있습니다.
FROM nginx
EXPOSE 443

Image Layer

  • Image layer는 햄버거와 유사합니다. 햄버거가 빵 -> 야채 -> 패티 -> 야채 -> 소스 -> 빵 순서로 하나씩 쌓아가듯 컨테이너도 레이어를 쌓아서 Image Layer를 이룬다고 생각하시면 됩니다.
  • 이미지 전송시간을 줄이기 위해 Read-only image layer 를 공유합니다.

위 그림에서 첫번째 그림의 Container에서 Image 1.0은 Read only 입니다. 여기에 Apache를 추가하여 레이어를 쌓았습니다.
두번째 그림은 첫번째 그림의 Container(Image 1.0, Iamge 1.1) 위에 Git을 추가하여 레이어를 쌓았습니다.
세번째 그림은 두번째 그림의 Container(Image1.0, 1.1, 1.2) 위에 Source를 추가하여 레이어를 쌓았습니다.
이렇게 명령어가 하나씩 실행될 때마다 upper layer(프로그램 설치)가 반복이 됩니다.



이런식으로 끊임없이 레이어가 쌓이게 되면 메모리는 점점 많아져 무거워 질겁니다.



예를 들어 보겠습니다.


1) dockerfile은 아래와 같습니다.

FROM ubuntu
RUN apt-get update && apt-get install -y -q nginx
RUN rm -rf /var/lib/apt/lists/***
COPY index.html /var/www/html
CMD ["nginx", "-g", "daemon off; " ]




2) dockerfile을 실행 후 모습

180MB의 메모리를 지니고 있는 것을 확인할 수 있습니다.



3) dockerfile 내의 RUN 명령어를 한줄로 변경하기

FROM ubuntu
RUN apt-get update && \
    apt-get install -y -q nginx && \
    rm -rf /var/lib/apt/lists/***
COPY index.html /var/www/html
CMD ["nginx", "-g", "daemon off; " ]




4) dockerfile 실행 후 모습

134MB 메모리로 줄어든 것을 확인할 수 있습니다!


java 파일을 Dockerfile을 이용한 예시 입니다.

  • 환경으로는 Springboot 2.5.2, java 11, gradle 6.8.2 에서 실행하였습니다.

build.gradle 파일

buildscript {
    dependencies {
        classpath "io.spring.gradle:dependency-management-plugin:0.5.1.RELEASE"
    }
}

plugins {
    id 'org.springframework.boot'
    id 'java'
    id 'com.palantir.docker' version '0.25.0'
}

compileJava{
    sourceCompatibility = 11
    targetCompatibility = 11
}

apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.pay.membership'
version = '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
    implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
    testImplementation 'com.tngtech.archunit:archunit:1.0.1'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation group: 'javax.persistence', name: 'javax.persistence-api', version: '2.2'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.jetbrains:annotations:23.0.0'
    testImplementation 'junit:junit:4.13.1'

    runtimeOnly 'mysql:mysql-connector-java'
    implementation project(path: ':common')

}

test {
    useJUnitPlatform()
}

docker{
    println(tasks.bootJar.outputs.files)

    // 이미지 이름
    name rootProject.name+'-'+project.name + ":" + version

    // 어떤 Dockerfile을 사용 할 거냐
    dockerfile file('../Dockerfile')

    // 어떤 파일들을 Dockerfile 에 복사하여 image에 넣을 것이냐
    files tasks.bootJar.outputs.files

    // Dockerfile 에 전달할 인자
    buildArgs(['JAR_FILE': tasks.bootJar.outputs.files.singleFile.name])
}

Docker File

FROM openjdk:11-slim-stretch
EXPOSE 8080
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

docker 실행

  • dockerfile 경로에서 아래 명령어를 실행합니다.

./gradlew docker


결과

Docker Compose(도커 컴포즈)

  • Docker Compose는 여러 개의 컨테이너(Container)로 구성된 애플리케이션을 관리하기 위한 간단한 오케스트레이션(Orchestration) 도구 입니다.
  • 또한 여러 개의 컨테이너를 yaml 파일의 코드를 통해 통합 관리하기 위한 툴입니다.

yaml

  • 구조화 데이터나 오브젝트를 문자열로 변환하기 위한 데이터 형식의 한 종류입니다.
  • Space를 사용한 들여쓰기를 통해 데이터 계층을 표시합니다.

Docker Compose 파일의 구조

version: ()
services: (docker container)
volumes: (docker volume)
networks: (docker network)
  • version: 지원 버전
  • services: 컨테이너 설정
  • volumes: 도커 볼륨에 대한 설정
  • networks: 도커 네트워크에 대한 설정

예시

version: '3'
services:
  mysql:
    image: mysql:8.0
    networks:
      - pay_network
    volumes:
      - ./db/conf.d:/etc/mysql/conf.d
      - ./db/data:/var/lib/mysql
      - ./db/initdb.d:/docker-entrypoint-initdb.d
    env_file: .env
    ports:
      - "3307:3306"
    environment:
      - TZ=Asia/Seoul
      - MYSQL_ROOT_PASSWORD=root@@
      - MYSQL_USER=shbae
      - MYSQL_PASSWORD=shbae@@

  membership-service:
    image: msapay-membership-service:1.0.0
    networks:
      - pay_network
    ports:
      - "8081:8080" # 외부에서 8081 port를 통해서 내부의 8080 port로 통할거야. 8080은 실제 어플리케이션 port
    depends_on:
      - mysql
    environment:
      - AXON_AXONSERVER_SERVERS=axon-server:8124
      - AXON_SERIALIZER_EVENTS=jackson
      - AXON_SERIALIZER_MESSAGES=jackson
      - AXON_SERIALIZER_GENERAL=xstream
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/msa_pay?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=shbae
      - SPRING_DATASOURCE_PASSWORD=shbae@@
      - SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT=org.hibernate.dialect.MySQL5InnoDBDialect
      - SPRING_JPA_HIBERNATE_DDL_AUTO=update

  banking-service:
    image: msapay-banking-service:1.0.0
    networks:
      - pay_network
    ports:
      - "8082:8080" # 외부에서 8081 port를 통해서 내부의 8080 port로 통할거야. 8080은 실제 어플리케이션 port
    depends_on:
      - mysql
    environment:
      - AXON_AXONSERVER_SERVERS=axon-server:8124
      - AXON_SERIALIZER_EVENTS=jackson
      - AXON_SERIALIZER_MESSAGES=jackson
      - AXON_SERIALIZER_GENERAL=xstream
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/msa_pay?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=shbae
      - SPRING_DATASOURCE_PASSWORD=shbae@@
      - SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT=org.hibernate.dialect.MySQL5InnoDBDialect
      - SPRING_JPA_HIBERNATE_DDL_AUTO=update

networks:
  pay_network:
    driver: bridge

Docker Compose Commands

  • docker-compose.yaml 파일이 위치한 디렉토리에서 실행

docker-compose up -d # background에서 서비스와 관련된 컨테이너 생성 및 시작

docker-compose down # 서비스와 관련된 컨테이너 종료 후 제거

docker-compose start/stop/restart # 서비스 관련 컨테이너 시작 / 종료 / 재시작

docker-compose kill # 실행 중인 컨테이너에 SIGKH 시그널을 통해서 강제 종료

docker-compose pause / unpause # 컨테이너 정지 / 재가동

반응형

'개발 > Docker' 카테고리의 다른 글

Docker vs 가상머신(Docker와 가상머신의 차이)  (0) 2023.09.27

응집도(Cohesion)란?

  • 응집도는 하나의 모듈이나 클래스 내부의 기능들이 얼마나 밀접하게 연관되어 있는지를 나타냅니다.
  • 즉, 하나의 모듈이 단일 기능이나 목적을 가지고 있는 정도를 말합니다.
  • 응집도가 높다는 것은 모듈 내의 모든 요소가 서로 긴밀하게 연결되어 있다는 것을 의미하며, 유지보수와 이해하기 쉬운 코드를 만드는데 중요한 역할을 합니다.

응집도 예시

  1. 높은 응집도
  • '계산기' 클래스에는 수학적 계산과 관련된 메소드들만 포함되어 있습니다.
  • '더하기', '빼기', '곱하기', '나누기' 등의 기능이 계산기 클래스에 포함됩니다.
  • '더하기', '빼기', '곱하기', '나누기' 각 메소드들은 계산기의 기능에 밀접하게 연관되어 있는 기능들로 응집도가 높습니다.
  1. 낮은 응집도
  • '도구상자' 클래스에는 파일을 읽는 메소드, 그래픽을 그리는 메소드, 데이터베이스 쿼리를 실행하는 메소드 등 서로 다른 기능들이 있습니다.
  • 이런식으로 하나의 기능을 특정 짓는 것이 아닌 관련없는 여러 기능들을 하나의 클래스에 선언할 때 응집도가 낮다라고 합니다.

결합도(Coupling)란?

  • 결합도는 서로 다른 모듈 간의 '의존성' 정도를 나타냅니다.
  • 모듈 간의 결합도가 낮다는 것은 각 모듈이 서로 독립적으로 기능한다는 것을 의미하며, 유지보수성과 확장성에 긍정적인 영향을 미칩니다.

결합도 예시

  1. 낮은 결합도
  • '사용자 인터페이스' 모듈과 '데이터 처리' 모듈이 서로 독립적으로 작동합니다.
  • 이는 하나의 모듈을 수정하더라도 다른 모듈에 영향을 주지 않는다는 것을 의미합니다.
  1. 높은 결합도
  • '주문 처리' 모듈이 '재고 관리' 모듈에 강하게 의존하고 있습니다.
  • 이 경우, '재고 관리' 모듈에 변화가 생기면 '주문 처리' 모듈도 이에 맞춰 변경해야 합니다.

응집도와 결합도의 균형

  • 효과적인 소프트웨어 설계를 위해서는 응집도를 높이고 결합도를 낮추는 것이 중요합니다.
  • 높은 응집도는 모듈이 명확하고 관리하기 쉽게 만들어 주며, 낮은 결합도는 시스템의 다른 부분에 영향을 주지 않고 독립적으로 모듈을 수정하거나 교체할 수 있게 해줍니다.
반응형

'개발 > Spring' 카테고리의 다른 글

AOP란? AOP 내용, AOP 예제  (0) 2024.01.11
[Querydsl] DTO 조회 방법  (0) 2023.12.17
OAuth2란? OAuth2 예시  (1) 2023.12.15
DTO <-> Entity 변환 이유!  (0) 2023.12.14

Github Label 이란?

  • Github label은 issue 와 pull request에서 카테고리를 분류하기 위한 이름표 라고 생각하시면 됩니다.

  • 처음 git repository를 생성하게 되면 label은 아래와 같이 기본적으로 9개의 label 들을 제공합니다.
  1. 상단 탭의 issues 클릭
  2. labels 버튼 클릭

Github Label 사용

  • Issues 작업을 진행할 때 협업하는 사람끼리 각 Issues 에 대해 어떤 작업인지 명확하게 표시하기 위해 사용합니다.
  • Issues 작업에서 Label을 기반으로 필터링 하여 검색할 수 있습니다.

  • Pull requests에서 Label을 기반으로 필터링 하여 검색할 수 있습니다.

Github Label 한번에 적용하는 법

환경

  • windows 10

1. lables.json 파일 준비하기

  • name: 라벨 이름, 이모지 포함 가능
  • coler: Hex 값으로 색상 지정 가능
  • description: 설명, 라벨 설명 지정 가능
[
  {
    "name": "⚙ Setting",
    "color": "e3dede",
    "description": "개발 환경 세팅"
  },
  {
    "name": "✨ Feature",
    "color": "a2eeef",
    "description": "기능 개발"
  },
  {
    "name": "🌏 Deploy",
    "color": "C2E0C6",
    "description": "배포 관련"
  },
  {
    "name": "🎨 Html&css",
    "color": "FEF2C0",
    "description": "마크업 & 스타일링"
  },
  {
    "name": "🐞 BugFix",
    "color": "d73a4a",
    "description": "Something isn't working"
  },
  {
    "name": "💻 CrossBrowsing",
    "color": "C5DEF5",
    "description": "브라우저 호환성"
  },
  {
    "name": "📃 Docs",
    "color": "1D76DB",
    "description": "문서 작성 및 수정 (README.md 등)"
  },
  {
    "name": "📬 API",
    "color": "D4C5F9",
    "description": "서버 API 통신"
  },
  {
    "name": "🔨 Refactor",
    "color": "f29a4e",
    "description": "코드 리팩토링"
  },
  {
    "name": "🙋‍♂️ Question",
    "color": "9ED447",
    "description": "Further information is requested"
  },
  {
    "name": "🥰 Accessibility",
    "color": "facfcf",
    "description": "웹접근성 관련"
  },
  {
    "name": "✅ Test",
    "color": "ccffc4",
    "description": "test 관련(storybook, jest...)"
  }
]

2. npm 설치

npm 설치 사이트로 이동하기 를 클릭하여 download 페이지로 이동해 주세요.

위 그림에서 자신의 환경에 맞는 npm을 설치하시면 됩니다.

3. Github access token 준비하기

해당 링크를 클릭하여 Github access token 발급하기 로 접속하여 access tokens 를 발급 받습니다.

  • Note: token 이름입니다. 각자의 목적에 맞게 작성해주시면 됩니다.
  • repo 카테고리를 체크 해주세요!

4. github-label-sync 설치

npm install -g github-label-sync

5. repository 적용

github-label-sync --access-token [액세스 토큰] --labels labels.json [계정명]/[저장소 이름]

예를 들어 액세스 토큰: abcd, 계정명: test, 저장소 이름: testRepo 라면

github-label-sync --access-token abcd --labels labels.json test/testRepo

Github에 label 한번에 적용하기 결과

  • label.json 에 입력한 label 들이 모두 적용 되었다.
  • 기존 label 들은 삭제 되니 주의!

반응형

Projection 연산이란?

한 Relation의 Attribute들의 부분 집합을 구성하는 연산자입니다.
결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가집니다.
결과 Relation은 기본 키가 아닌 Attribute에 대해서만 중복된 tuple들이 존재할 수 있습니다.
-> 테이블에서 원하는 컬럼만 뽑아서 조회하는 것

  • Relation 데이터를 원자 값으로 갖는 이차원 테이블
  • Column = Attribute

Querydsl DTO로 조회하는 4가지 방법

  1. Projection.bean
  2. Projection.fields
  3. Projection.constructor
  4. @QueryProjection

사전 작업

  • spring boot3.0

  • build.gradle

    buildscript {
      ext {
          queryDslVersion = "5.0.0"
      }
    }
    

plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'

// querydsl관련 명령어를 gradle탭에 생성해준다. (권장사항)
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"

}

group = 'com.demo'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// querydsl 디펜던시 추가
// == 스프링 부트 3.0 이상 ==
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

}

tasks.named('test') {
useJUnitPlatform()
}

/*

  • queryDSL 설정 추가
  • /
    // querydsl에서 사용할 경로 설정
    def querydslDir = "$buildDir/generated/querydsl"
    // JPA 사용 여부와 사용할 경로를 설정
    querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
    }
    // build 시 사용할 sourceSet 추가
    sourceSets {
    main.java.srcDir querydslDir
    }
    // querydsl 컴파일시 사용할 옵션 설정
    compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
    }
    // querydsl 이 compileClassPath 를 상속하도록 설정
    configurations {
    compileOnly {
      extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
    }

// gradle clean 시에 QClass 디렉토리 삭제
clean{
delete file(querydslDir)
}



- Entity

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Users {
@Id
private Long userNo;
private String userId;
private String userName;
}

@Getter @Setter
@NoArgsConstructor
@RequiredArgsConstructor
public class Board {
@Id
private Long id;
private String title;
private String content;
}


- DTO

@Getter @Setter
@NoArgsConstructor
public class UsersDto {
private Long userNo;

private String userId;

private String userName;

@QueryProjection
public UsersDto(Long userNo, String userId, String userName) {
    this.userNo = userNo;
    this.userId = userId;
    this.userName = userName;
}

}


- JpaQueryFactory Config

@Configuration
@RequiredArgsConstructor
public class QuerydslConfiguration {

private final EntityManager em;

@Bean
public JPAQueryFactory jpaQueryFactory(){
    return new JPAQueryFactory(em);
}

}


#### 1. Projections.bean (Setter 메서드를 이용한 조회 방법)

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> users = queryFactory
        .select(Projections.bean(UsersDto.class,
            users.userNo,
            users.userId,
            users.userName
        ))
        .from(users)
        .fetch();
}

}


- Projection.bean 방법은 setter 메서드를 기반으로 동작합니다.
DTO 객체의 각 필드에 setter 메서드가 있어야 합니다.
만약 영속화된 데이터를 변경하는 책임을 가진 객체라면 문제가 될 수 있기 때문에 권장되는 패턴은 아닙니다.

#### 2. Projections.fields (Reflection을 이용한 조회 방법)

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> users = queryFactory
        .select(Projections.fields(UsersDto.class,
            users.userNo,
            users.userId,
            users.userName.as("userName")
        ))
        .from(users)
        .fetch();
}

}


- Porjections.fields를 이용한 방법은 field에 값을 직접 주입해주는 방식입니다.
Projections.bean 방식과 마찬가지로 Type이 다를 경우 매칭되지 않으며, 컴파일 시점에서는 에러를 잡지 못하고 런타임 시점에서 에러가 잡힙니다.

- 만약 Users Entity의 필드가 userName이 아니라면 users.userName.as("userName") 처럼 alias(별칭)을 사용하여 매핑 문제를 해결할 수 있습니다.

#### 3. Projections.constructor (생성자를 이용한 조회 방법)

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> users = queryFactory
        .select(Projections.constructor(UsersDto.class,
            users.userNo,
            users.userId,
            users.userName
        ))
        .from(users)
        .fetch();
}

}


- Projections.constructor는 생산자 기반 바인딩입니다. 생성자 기반 바인딩이기 때문에 객체의 불변성을 가져갈 수 있다는 장점이 있지만 바인딩 과정에서 문제가 생길 수 있습니다.

public static ConstructorExpression constructor(Class type, Expression... exprs){
return new ConstructorExpression(type, exprs);
}


- Projections의 constructor 메서드입니다. 해당 메서드를 보면 DTO 객체의 생성자에게 직접 바인딩하는 것이 아니라 Expression<?>... exprs 값을 넘기는 방식으로 작동됩니다.
따라서 값을 넘길 때 생성자와 필드의 순서를 일치시켜야 합니다.
필드의 개수가 적을 때는 문제가 되지 않지만 필드의 개수가 많아지는 경우 오류로 이어질 수 있습니다.
하지만 생성자를 이용한 방식은 필드명이 달라도 해당 순서에 위치한 필드의 타입만 서로 일치한다면 정상적으로 동작한다는 특징이 있습니다.


#### 4. @QueryProjection

DTO

@Getter @Setter
@NoArgsConstructor
public class UsersDto {
private Long userNo;

private String userId;

private String userName;

@QueryProjection
public UsersDto(Long userNo, String userId, String userName) {
    this.userNo = userNo;
    this.userId = userId;
    this.userName = userName;
}

}

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> dtos = queryFactory
            .select(new QUsersDto(
                    users.userNo,
                    users.userId,
                    users.userName
            ))
            .from(users)
            .fetch();
}

}

```

  • @QueryProjection을 이용하면 불변 객체 선언, 생성자를 그대로 사용할 수 있기 때문에 권장되는 패턴입니다.
    (정확하게는 DTO의 생성자를 사용하는 것이 아니라 DTO 기반으로 생성된 QDTO 객체의 생성자를 사용하는 것입니다.)

  • 작동 방법은 먼저 DTO 생성자에 @QueryProjection 어노테이션을 추가하여 QType의 클래스를 생성하여 위의 예시 코드(UsersDto)와 같이 사용합니다. 이 방식은 new QDTO로 사용하기 때문에 런타임 에러뿐만 아니라 컴파일 시점에서도 에러를 잡아주고, 파라미터로 확인할 수 있다는 장점이 있습니다.
    반면 해당 기능을 사용하려는 DTO마다 QType의 Class를 추가로 생성해줘야 하는 것도 있지만, DTO는 Repository 계층의 조회 용도뿐만 아니라 Service, Controller 모두 사용되기 때문에 @QueryProjection 어노테이션을 DTO에 적용시키는 순간, 모든 계층에서 쓰이는 DTO가 Querydsl에 의존성을 가지기 때문에 아키텍처적으로 적용을 생각해 볼 필요는 있습니다.

반응형

'개발 > Spring' 카테고리의 다른 글

AOP란? AOP 내용, AOP 예제  (0) 2024.01.11
응집도와 결합도란?  (1) 2023.12.26
OAuth2란? OAuth2 예시  (1) 2023.12.15
DTO <-> Entity 변환 이유!  (0) 2023.12.14

+ Recent posts