https://velog.io/@as9587/AWS-EC2-Amazon-Linux-2023-OS-%ED%8F%AC%ED%8A%B8-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%ED%8A%B8port-redirect-%ED%95%98%EB%A9%B0-%EB%B0%9C%EC%83%9D%ED%95%9C-%EC%9D%B4%EC%8A%88-%EC%A0%95%EB%A6%AC

 

[AWS EC2 - Amazon Linux 2023 OS] 포트 리다이렉트(port redirect )하며 발생한 이슈 정리

AWS EC2의 포트 리다이렉트를 하면서 겪은 이슈를 이야기 하고, 어떤 것이 문제였는지 알아보겠습니다.제가 진행중인 사이드 프로젝트를 AWS EC2에 배포 후, 80 포트로 접속을 하더라도 8080포트로 접

velog.io

 

 

구세주 같은 분을 만나 포트포워딩이 극적으로 됐다.

나중에 혹~~시라도 같은 문제가 발생할까 싶어 여기다 저장해놓습니다요!

감사합니다 작성자분 ㅠ _ ㅠ

OAuth는 접근 위임을 위한 인증방식 표준이다.

 

내가 개발한 서비스에 구글/카카오 로그인을 한다면 그 고객이 구글과 카카오 회원임을 어떻게 하면 알 수 있을까?

 

1) 사용자의 구글 아이디/패스워드를 알아내서 로그인 한다? No~ 

2) 고객의 구글 로그인 정보를 안전한 방법으로 우리 서비스에 전달하여 로그인 한다? Yes

 

  • 여기서 말하는 안전한 방법이란?

고객이 구글에 로그인을 한다 -> Access Token이 발급된다 -> 리다이렉트를 통해 구글 로그인에서 우리 서비스로 이동하는데 토큰을 리다이렉트되는 주소에 담아서 보낸다.

 

위의 방법으로는 사실 해킹의 위험이 있다. 리다이렉트 되는 주소를 중간에서 바꿔버릴 수가 있기 때문이다. 

이런 위험 때문에 OAuth를 사용하는 것이고, OAuth 등록 절차가 사전에 필수적으로 이뤄져야 한다. 

등록 절차는 리다이렉트 되는 uri와 같은 것을 외부 서비스에 등록하는 것이다. 

 

외부 서비스(구글/카카오/네이버)에 OAuth 등록 절차를 밟는다 -> 고객이 구글에 로그인을 한다 -> AccessToken이 발급된다 -> 미리 등록한 리다이렉트 주소로 Access Token을 전달한다 -> 서버는 AccessToken을 Header에 담아 외부 서비스에 회원 정보 요청을 한다 -> 정보를 건네준다. 

 

https://velog.io/@undefcat/OAuth-2.0-%EA%B0%84%EB%8B%A8%EC%A0%95%EB%A6%AC

 

 

지정한 job만 실행될 수 있도록 application.yml에는 아래 내용을 추가

spring:
  batch:
    job:
      names: ${job.name:NONE}

main 함수에는 아래와 같은 job이 실행된 다음 종료될 수 있는 코드를 추가하였다.

 ConfigurableApplicationContext applicationContext = SpringApplication.run(TestBatchApplication.class, args);
        System.exit(SpringApplication.exit(applicationContext));

 

Dashboard > Jenkins관리 > Configure System 에서 아래의 설정을 해준다. 

1. Global properties (jar위치, jvm 옵션 등등)

2. Build Timestamp

Timestamp 플러그인을 다운 받아 아래와 같이 설정 

Jenkins에서 Freestyle Project를 선택해서 shell script를 실행하게 하였고,

AWS EC2에 있는 jar 파일을 직접 실행하게끔 하였다. 

 

지정한 jar파일에 접근할 수 있도록 권한 주기

sudo chown -R ec2-user:ec2-user /var/lib/jenkins/workspace/

 

 

위에서 구성한 Jenkins Project들의 이름은 Pipeline 스크립트에서 사용된다.

 

파이프라인 설정

 

Pipeline 아이템 추가

 

파이프라인을 설정해주는데 나중에는 Trigger는 'Build periodically'를 선택해 일정 주기로 실행될 수 있게 해줄 것이다. 

파이프라인 스크립트는 아래와 같이 간단하게 작성해본다.

pipeline {
    agent none
    stages {
        stage('a') {
            steps {
                build 'ATargetJob'
            }
        }
          stage('b') {
            steps {
                build job: "BTargetJob", wait : true
            }
        }
        stage('c') {
            steps {
                build job: "CTargetJob", wait : true
            }
        }
        stage('d') {
            steps {
                build job: "DTargetJob", wait : true
            }
        }
    }
}

 

순차적으로 a->b->c->d가 실행되고, 중간에 실패되면 pipeline은 중지된다. 

AWS EC2 환경에 설치된 Jenkins로 실행하다 보니 로컬 환경에서와는 달리 실패되는 부분이 있었고, 결국 11번의 시도 끝에 pipeline이 정상적으로 실행되었다. 

 

 

 

이제 큰 틀은 짰으니 세세한 부분을 신경쓰러 떠나보겠다...

Java 11에서는 try-with-resources 문을 사용하여 BufferedReader를 생성하고 자동으로 닫을 수 있다.

BufferedReader br = new BufferedReader(new FileReader(queryPath));
try {
    StringBuilder sb = new StringBuilder();
    String line;

    while ((line = br.readLine()) != null) {
        sb.append(line).append("\n");
    }
    return sb.toString();
} catch (Exception e) {
    throw e;
} finally {
    br.close();
}

위 코드를 아래와 같이 변경해보았다.

try-with-resources 문을 사용하면 BufferedReader를 명시적으로 닫을 필요가 없으며

또한 IOException만 처리하면 되므로 예외처리도 간단해진다.

try (BufferedReader br = new BufferedReader(new FileReader(queryPath))) {
    StringBuilder sb = new StringBuilder();
    String line;

    while ((line = br.readLine()) != null) {
        sb.append(line).append("\n");
    }

    return sb.toString();
} catch (IOException e) {
    throw e;
}

Files와 Stream을 이용해 더 짧게 바꿀 수도 있다.

Java 8 이상에서 가능한 Files 클래스와 Stream API를 사용하여 파일을 읽고 문자열로 변환하는 방법이다.

Files.lines() 메서드는 내부적으로 파일을 열고 자동으로 닫아서 명시적으로 뭘 안닫아도 된다~~

try {
    String sqlString = Files.lines(Paths.get(queryPath))
                           .collect(Collectors.joining("\n"));
    return sqlString;
} catch (IOException e) {
    throw e;
}

Java에서 wait, notify, notifyAll 메소드와 모니터에 대해 알아보겠습니다.


동작 방식
Java에서 스레드 간의 동기화를 위해서는 synchronized 블록이나 메소드를 사용하여 임계영역(critical section)을 정의해야 합니다. 이러한 임계영역에서는 단일 스레드만 접근할 수 있으며, 다른 스레드는 대기 상태가 됩니다. wait, notify, notifyAll 메소드는 이러한 대기 상태의 스레드를 관리하기 위해 사용됩니다.

wait 메소드는 스레드를 일시적으로 대기 상태로 전환합니다. wait 메소드를 호출한 스레드는 해당 객체의 모니터를 해제하고, 대기 상태로 전환합니다. 다른 스레드가 해당 객체의 모니터를 잡으면, 대기 중인 스레드는 다시 실행 가능한 상태가 됩니다.

notify 메소드는 대기 중인 스레드 중 하나를 깨웁니다. 이때, 깨워지는 스레드는 해당 객체의 모니터를 다시 잡고, 대기 상태에서 벗어나 실행 가능한 상태가 됩니다. notify 메소드를 호출한 스레드가 해당 객체의 모니터를 유지하고 있어야 합니다.

 

notifyAll 메소드는 대기 중인 모든 스레드를 깨웁니다. 이때, 깨워지는 스레드들은 모두 해당 객체의 모니터를 다시 잡고, 대기 상태에서 벗어나 실행 가능한 상태가 됩니다.

 

간단히 말해서 wait()를 호출하면  다른 스레드가 같은 객체에 대해 notify() 또는 notifyAll()을 호출할 때까지 현재 스레드가 강제로 대기합니다.

모니터에 대해 자세히 알아보자.

  • Java에서 monitor란 스레드 동기화를 위한 개념 중 하나로, 모니터는 임계 영역(critical section)에 대한 접근을 제어하기 위한 객체입니다. 모니터는 단일 스레드만이 접근할 수 있는 임계 영역을 정의하고, 다른 스레드들은 해당 모니터에 대한 접근 권한을 획득하기 위해 대기 상태가 됩니다.
  • Java에서 모니터는 synchronized 키워드로 구현됩니다. synchronized 키워드를 이용하여 메소드나 블록을 동기화하면, 해당 메소드나 블록에는 모니터 객체가 생성됩니다. 이 모니터 객체는 해당 메소드나 블록의 실행을 단일 스레드로 제한하는 역할을 합니다.

 

  • 특징
    • 모니터는 단일 스레드만이 실행할 수 있는 임계 영역을 제공합니다.
    • 모니터는 synchronized 키워드를 사용하여 구현됩니다.
    • 모니터는 스레드 동기화를 위한 기본적인 개념입니다.
    • 모니터는 스레드 간의 경쟁 조건(race condition)을 해결하기 위해 사용됩니다.
    • 다른 스레드들은 해당 모니터에 대한 접근 권한을 획득하기 위해서는 대기 상태가 되어야 합니다. 대기 중인 스레드들 중에서 우선순위가 높은 스레드가 모니터의 lock을 획득하고, 임계 영역에 접근하여 실행합니다. 이후, 다른 스레드들은 다시 대기 상태가 됩니다. 이를 기아 상태(starvation)라고 부르며, 이를 해결하기 위해서는 공정한 스케줄링이 필요합니다.

 

https://www.baeldung.com/java-wait-notify

하루 그리고 반나절을 투자해 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 키워드를 이용해 기존 커피 객체의 설명과 가격을 먼저 가져오고, 이에 추가적인 설명과 가격을 더해줍니다. 마지막으로 클라이언트 코드에서는 다양한 데코레이터 객체를 조합하여 원하는 커피를 생성할 수 있습니다.

+ Recent posts