하루 그리고 반나절을 투자해 Spring Batch를 Spring Cloud Data Flow로 관리하려고 했다. 배치 잡 실행과 모니터링을 중앙에서 관리하고, UI도 심플하니 맘에 들어서였다. 그러나 결론은 실패..... 실패한 이유는 사실 프로젝트 일정에 맞추기엔 docker에 대한 사전 지식 부족으로 시간을 무한정 투자할 수가 없어서였다.

 

우선 SCDF설치를 하는 방법엔 여러가지가 있는데 (Docker Compose, Cloud Foundry, Kubernetes) 그 중에서 나는 docker를 이용해 로컬에 설치하는 방법을 택했다. 

https://dataflow.spring.io/docs/installation/local/docker/

 

Spring Cloud Data Flow(SCDF) 서버에 배치 잡 애플리케이션을 등록하려면 다음과 같은 단계를 따를 수 있다.

  1. 배치 잡 애플리케이션 빌드
    1. Spring Batch를 사용하여 배치 잡을 작성합니다.
    2. 배치 잡 애플리케이션을 빌드하여 실행 가능한 JAR 파일을 생성합니다.
  2. SCDF 서버 설치
    1. 도커로 설치
  3. 애플리케이션 등록
    1. SCDF 서버 대시보드에서 "Create App" 버튼을 클릭하여 애플리케이션을 등록합니다.
      "Type"을 "batch"로 선택하고, "Name"에 애플리케이션 이름을 입력하고, "URI"에 빌드한 JAR 파일의 경로를 입력합니다. (애플리케이션을 등록하면 SCDF 서버에 배치 잡 애플리케이션을 등록한 것)
  4. 배치 잡 실행
    1. SCDF 서버 대시보드에서 배치 잡 애플리케이션을 선택하고 실행을 시작합니다.
    2. 실행 구성 및 배치 잡 인수를 지정할 수 있습니다.

 

컴퓨터에 docker설치도 안되어 있던 터라 도커부터 설치를 해주고.. 

아래 docker-compose 파일을 실행해준다. 해당 파일에는 mysql, rabbitmq, dataflow-server, skipper-server, promateus, grafana 서비스를 등록한 내용이 담겨있다. spring cloud data flow를 설치하려면 db, messaging, dataflow-server,skipper-server가 필요하고 scheduling을 위해선 promateus, grafana 설정이 추가로 필요하다. 

 

version: '3'

services:
  mysql:
    image: mysql:5.7.25
    container_name: dataflow-mysql
    environment:
      MYSQL_DATABASE: dataflow
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: rootpw
    networks:
      - dataflow-network
    expose:
      - '3306'
    ports:
      - '3306:3306'

  rabbitmq:
    image: rabbitmq:3.7.17-management-alpine
    container_name: dataflow-rabbitmq
    networks:
      - dataflow-network
    ports:
      - '5672:5672'
      - '15672:15672'

  dataflow-server:
    image: springcloud/spring-cloud-dataflow-server:2.2.1.RELEASE
    container_name: dataflow-server
    volumes:
      - "./tmp:/tmp"
      - "./workspace:/workspace"
    networks:
      - dataflow-network
    ports:
      - "9393:9393"
    environment:
      - spring.cloud.dataflow.applicationProperties.stream.spring.rabbitmq.host=rabbitmq
      - spring.cloud.skipper.client.serverUri=http://skipper-server:7577/api
      - spring.cloud.dataflow.applicationProperties.stream.management.metrics.export.prometheus.enabled=true
      - spring.cloud.dataflow.applicationProperties.stream.spring.cloud.streamapp.security.enabled=false
      - spring.cloud.dataflow.applicationProperties.stream.management.endpoints.web.exposure.include=prometheus,info,health
      - spring.cloud.dataflow.grafana-info.url=http://localhost:3000
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/dataflow
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=rootpw
      - SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
    depends_on:
      - rabbitmq
    entrypoint: "./wait-for-it.sh mysql:3306 -- java -jar /maven/spring-cloud-dataflow-server.jar"

  skipper-server:
    image: springcloud/spring-cloud-skipper-server:2.1.2.RELEASE
    container_name: skipper
    volumes:
      - "./tmp:/tmp"
      - "./workspace:/workspace"
    networks:
      - dataflow-network
    ports:
      - "7577:7577"
      - "9000-9010:9000-9010"
      - "20000-20105:20000-20105"
    environment:
      - SPRING_CLOUD_SKIPPER_SERVER_PLATFORM_LOCAL_ACCOUNTS_DEFAULT_PORTRANGE_LOW=20000
      - SPRING_CLOUD_SKIPPER_SERVER_PLATFORM_LOCAL_ACCOUNTS_DEFAULT_PORTRANGE_HIGH=20100
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/dataflow
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=rootpw
      - SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
    entrypoint: "./wait-for-it.sh mysql:3306 -- java -Djava.security.egd=file:/dev/./urandom -jar /spring-cloud-skipper-server.jar"

  # Grafana is configured with the Prometheus datasource.
  # Use `docker exec -it prometheus /bin/sh` to log into the container
  prometheus:
    image: springcloud/spring-cloud-dataflow-prometheus-local:2.2.1.RELEASE
    container_name: prometheus
    volumes:
      - 'scdf-targets:/etc/prometheus/'
    networks:
      - dataflow-network
    ports:
      - '9090:9090'
    depends_on:
      - service-discovery

  # The service-discovery container. Required for Prometheus setup only
  # Use `docker exec -it service-discovery /bin/sh` to log into the container
  service-discovery:
    image: springcloud/spring-cloud-dataflow-prometheus-service-discovery:0.0.4.RELEASE
    container_name: service-discovery
    volumes:
      - 'scdf-targets:/tmp/scdf-targets/'
    networks:
      - dataflow-network
    expose:
      - '8181'
    ports:
      - '8181:8181'
    environment:
      - metrics.prometheus.target.cron=0/20 * * * * *
      - metrics.prometheus.target.filePath=/tmp/scdf-targets/targets.json
      - metrics.prometheus.target.discoveryUrl=http://dataflow-server:9393/runtime/apps
      - metrics.prometheus.target.overrideIp=skipper-server
      - server.port=8181
    depends_on:
      - dataflow-server

  # Grafana SCDF Prometheus pre-built image:
  grafana:
    image: springcloud/spring-cloud-dataflow-grafana-prometheus:2.2.1.RELEASE a
    container_name: grafana
    networks:
      - dataflow-network
    ports:
      - '3000:3000'

networks:
  dataflow-network:

volumes:
  scdf-targets:

 

docker -f XXX.yml up 

명령어를 통해 위 파일을 실행해주면 아래와 같이 http://localhost:9393/dashboard/ 에서 Data Flow 화면을 확인할 수 있다. 

여기서 더 이상의 진행은 중단했다. APP을 등록하는 것에서 애를 먹기도 먹거니와 이걸 결국에는 운영 EC2 환경에서 다양한 변수들을 고려하여 세팅을 해줘야 하는데 허들이 꽤나 있을 것으로 예상되었다. 솔직히 이것만 붙잡고 하고 싶은 마음이 굴뚝 같았으나 (승부욕 발동) 새로운 기술을 써보겠다는 개인적인 사리사욕보다는 우선은 프로젝트 일정에 맞추는 것 더 중요하다 판단되어 일단 이 정도까지 알아본 것으로 마무리하고, 원래 계획대로 Jenkins에서 Spring Batch를 실행하기로.. (쥬륵)너무 아쉽고, 꼭 프로젝트 마치고 다시 도전해봐야 겠다. 

 

Access-Control-Allow-Origin은 Cross-Origin Resource Sharing (CORS)를 구현하기 위해 사용되는 HTTP 응답 헤더 중 하나입니다. 이 헤더를 사용하여 다른 도메인에서 해당 자원에 접근할 수 있는 권한을 부여할 수 있습니다.

그러나, 모든 도메인에 대해 Access-Control-Allow-Origin: *와 같은 와일드카드를 사용하여 모든 도메인에서 접근을 허용하는 것은 보안상 위험할 수 있습니다. 왜냐하면, 이를 허용하면 악의적인 공격자들이 자신들의 도메인에서 해당 자원에 접근하여 보안에 취약한 정보를 탈취하거나, 다른 사용자들에게 피해를 줄 수 있기 때문입니다.

따라서, 보안상 취약성을 최소화하기 위해서는 필요한 경우에만 Access-Control-Allow-Origin을 특정 도메인으로 설정하고, 그 외의 도메인에서는 해당 자원에 대한 접근을 차단하는 것이 좋습니다.

 

특정 도메인만 Access-Control-Allow-Origin을 허용하려면, 

서버 측에서 해당 도메인만 허용하는 설정을 해주어야 합니다.

대부분의 서버에서는 CORS 설정을 통해 도메인별로 Access-Control-Allow-Origin 허용 여부를 설정할 수 있습니다.
Access-Control-Allow-Origin 허용 도메인을 example.com으로 설정하는 경우 아래와 같이 코드를 작성할 수 있습니다.

// HttpServletResponse 객체에 CORS 설정 추가
response.setHeader("Access-Control-Allow-Origin", "https://example.com");

만약, 여러 도메인을 허용하고 싶은 경우에는 Access-Control-Allow-Origin 값에 쉼표(,)로 구분하여 여러 도메인을 추가할 수 있습니다.

// HttpServletResponse 객체에 CORS 설정 추가
response.setHeader("Access-Control-Allow-Origin", "https://example.com, https://test.com");

또한, Spring Framework에서는 @CrossOrigin 어노테이션을 사용하여 특정 컨트롤러나 메서드에서 CORS 설정을 간편하게 적용할 수도 있습니다.

@RestController
public class MyController {

  // 특정 도메인만 허용하는 CORS 설정 적용
  @CrossOrigin(origins = "https://example.com")
  @GetMapping("/my-endpoint")
  public ResponseEntity<String> myEndpoint() {
    // ...
  }
}

Java Decorator Pattern은 객체 지향 디자인 패턴 중 하나로, 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator클래스로 정의 한 후 필요한 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계 하는 방식이다. 

 

Decorator 패턴은 기존 객체를 감싸는 새로운 데코레이터 클래스를 만들어서 기존 객체의 메소드를 호출하는 방식으로 동작합니다.이 패턴을 사용하면 기존 객체에 새로운 기능을 추가하기 위해 상속을 사용하는 대신, 런타임 시간에 객체의 기능을 동적으로 조작할 수 있습니다. 또한 여러 개의 데코레이터를 순차적으로 적용하여 새로운 기능을 더욱 다양하게 추가할 수 있습니다.

Java Decorator Pattern의 구성요소는 다음과 같습니다.

  1. Component: Decorator 패턴의 기초가 되는 인터페이스 또는 추상 클래스. Decorator 클래스와 ConcreteComponent 클래스가 이를 구현하게 됩니다.
  2. ConcreteComponent: Component의 구현체로서, 기본적인 기능을 제공하는 클래스입니다.
  3. Decorator: Component를 상속받는 추상 클래스로, 새로운 기능을 추가하기 위한 필드와 메소드를 가지고 있습니다. 또한 Component를 상속받는 모든 데코레이터 클래스의 공통점을 정의합니다.
  4. ConcreteDecorator: Decorator를 상속받는 실제 데코레이터 클래스로, 기존 객체에 새로운 기능을 추가합니다.

이렇게 구성된 Decorator 패턴은 객체 지향의 다형성을 이용하여 기존 객체에 새로운 기능을 추가하고 확장할 수 있으며, 코드의 유연성과 확장성을 높여줍니다.

 

// Component interface
public interface Coffee {
    String getDescription();
    double cost();
}

// Concrete Component
public class BasicCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Basic coffee";
    }

    @Override
    public double cost() {
        return 2.0;
    }
}

// Decorator
public abstract class CoffeeDecorator implements Coffee {
    private Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost();
    }
}

// Concrete Decorator
public class Milk extends CoffeeDecorator {
    public Milk(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", with milk";
    }

    @Override
    public double cost() {
        return super.cost() + 0.5;
    }
}

// Concrete Decorator
public class Sugar extends CoffeeDecorator {
    public Sugar(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", with sugar";
    }

    @Override
    public double cost() {
        return super.cost() + 0.25;
    }
}

// Client code
public class CoffeeShop {
    public static void main(String[] args) {
        Coffee basicCoffee = new BasicCoffee();
        System.out.println(basicCoffee.getDescription() + ": " + basicCoffee.cost());

        Coffee milkCoffee = new Milk(basicCoffee);
        System.out.println(milkCoffee.getDescription() + ": " + milkCoffee.cost());

        Coffee sugarCoffee = new Sugar(basicCoffee);
        System.out.println(sugarCoffee.getDescription() + ": " + sugarCoffee.cost());

        Coffee milkSugarCoffee = new Milk(new Sugar(basicCoffee));
        System.out.println(milkSugarCoffee.getDescription() + ": " + milkSugarCoffee.cost());
    }
}

위 코드는 Coffee 인터페이스를 구현한 BasicCoffee 클래스와 CoffeeDecorator 추상 클래스를 정의하고, 이를 상속받아 구체적인 데코레이터 클래스인 Milk와 Sugar를 구현합니다. 각 데코레이터 클래스에서는 커피의 설명과 가격을 반환할 때 super 키워드를 이용해 기존 커피 객체의 설명과 가격을 먼저 가져오고, 이에 추가적인 설명과 가격을 더해줍니다. 마지막으로 클라이언트 코드에서는 다양한 데코레이터 객체를 조합하여 원하는 커피를 생성할 수 있습니다.

자바 성능 튜닝 이야기를 보고 한 번 포스팅한 내용인데 다시 요약해보면서 리마인드!

 

 

String 클래스

  • String 클래스는 문자열을 불변(immutable)하게 다룹니다.
    • immutable 객체는 생성 후에 내부 상태를 변경할 수 없습니다. 문자열의 내용이 변경될 때마다 새로운 String 객체를 생성해야 한다는 것을 의미하며 이러한 객체는 변경할 수 없으므로 여러 스레드에서 동시에 접근하더라도 안전하게 사용할 수 있습니다.
  • 한 번 생성된 String 객체는 변경될 수 없으며, 문자열의 변경이 필요할 경우 새로운 String 객체를 생성합니다.
  • 이로 인해 String 클래스를 이용한 문자열 연산이 빈번하게 일어나는 경우, 성능이 저하될 수 있습니다. (GC 대상이 늘어남에 따라..메모리 사용을 최소화 하는 것은 당연한 일!)

StringBuffer 클래스

  • StringBuffer 클래스는 문자열을 가변(mutable)하게 다룹니다.
  • 문자열의 변경이 필요할 경우, 기존 StringBuffer 객체를 변경하여 새로운 문자열을 만들지 않고도 문자열을 수정할 수 있습니다.
  • 여러 개의 문자열을 결합하거나, 문자열의 일부를 삭제하거나, 변경하는 등의 작업에 용이합니다.
  • 멀티스레드 환경에서 안전하게 동작합니다.
  • 클래스에 static으로 선언한 문자열을 변경하거나, singleton으로 선언된 클래스에 선언된 문자열일 경우!

StringBuilder 클래스

  • StringBuilder 클래스는 StringBuffer 클래스와 마찬가지로 문자열을 가변하게 다룹니다.
  • 문자열의 변경이 필요할 경우, 기존 StringBuilder 객체를 변경하여 새로운 문자열을 만들지 않고도 문자열을 수정할 수 있습니다.
  • StringBuffer 클래스와의 차이점은 멀티스레드 환경에서 안전하지 않다는 점입니다. StringBuilder 클래스는 단일 스레드 환경에서 사용하기에 적합합니다.
  • 예를 들면 메서드 내에서 사용하는 문자열인 경우!

요약하자면, String 클래스는 문자열을 변경할 수 없고, StringBuffer 클래스와 StringBuilder 클래스는 문자열을 가변하게 다룰 수 있지만, StringBuffer 클래스는 멀티스레드 환경에서 안전하며 StringBuilder 클래스는 단일 스레드 환경에서 사용하기에 적합합니다.

 

 

 

꽤 오랜만에 JPA를 사용하는 프로젝트를 진행중인데

얼마나 시간이 지났다고 연관관계 mapping하는 방법이 가물가물하다; (예전에 그렇게 학을 떼어놓고는..)

살짝 헷가리는 어노테이션 정리하고 렛츠고!

 

mappedBy와 @JoinColumn은 JPA에서 엔티티 간 관계를 매핑할 때 사용하는 어노테이션입니다.

둘 다 관계 매핑에 필요한 정보를 제공하고 있지만, 다음과 같은 차이가 있습니다.

  • mappedBy: 양방향 관계에서 "다(N)" 쪽의 엔티티에서 사용되며, "일(1)" 쪽의 엔티티에 대한 매핑 정보를 지정합니다. 이를 통해 연관된 엔티티 사이에 양방향 참조를 설정할 수 있습니다.
  • @JoinColumn: "일(1)" 쪽의 엔티티에서 사용되며, 조인 컬럼 이름을 지정하여 연관된 엔티티의 외래 키 컬럼을 매핑합니다. 이를 통해 단방향 관계에서 연관된 엔티티의 외래 키를 지정할 수 있습니다.

즉, mappedBy는 양방향 관계에서만 사용되며, 연관된 엔티티 사이의 관계를 맺을 때 사용하고, @JoinColumn은 단방향 관계에서도 사용 가능하며, 외래 키 매핑에 사용됩니다.

그러나 두 어노테이션은 함께 사용되기도 합니다. 예를 들어, @ManyToOne으로 매핑된 엔티티 클래스에서 @JoinColumn 어노테이션을 사용하여 조인 컬럼을 지정하면, 이에 대응하는 @OneToMany으로 매핑된 엔티티 클래스에서는 mappedBy를 사용하여 역방향 참조를 설정하는 경우가 많습니다.

 

예를 들어, Team과 Member가 일대다 관계를 가지고 있다고 가정해봅시다. 이때 Team엔티티에서 Member엔티티의 리스트를 매핑하고, Member엔티티에서는 Team엔티티와의 관계를 역방향 매핑하려면 다음과 같이 mappedBy를 사용할 수 있습니다.

@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // getters, setters
}

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private Long id;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    // getters, setters
}

+ Recent posts