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

 

 

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

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() {
    // ...
  }
}
PostgreSQL

 

관계형 DBMS 중 하나로 MySQL이 오라클에 인수됨에 따라 요즘 더욱 각광을 받고 있는 추세이다.

주요 특징은 무료 오픈소스, 타 DBMS보다 안정성과 트랜잭션 및 ACID가 좋으며

JSONB나 ARRAY같은 타입을 사용할 수 있다는 장점이 있다. 

 

내가 사용하는 버전은 13.7으로 정확히 말하면 AmazonAurora Postgresql 13이다. Amazon Aurora(Aurora)는 MySQL 및 PostgreSQL과 호환되는 완전 관리형 관계형 데이터베이스 엔진이다 .

(Aurora는 볼륨 크기를 증가시킬 수 있고 또한 데이터베이스 구성 및 관리의 가장 어려운 측면 중 하나인 데이터베이스 클러스터링 및 복제를 자동화하고 표준화하는 이점이 있습니다).

 

더보기

ACID

  • 원자성(Atomicity)은 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력이다. 예를 들어, 자금 이체는 성공할 수도 실패할 수도 있지만 보내는 쪽에서 돈을 빼 오는 작업만 성공하고 받는 쪽에 돈을 넣는 작업을 실패해서는 안된다. 원자성은 이와 같이 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다.
  • 일관성(Consistency)은 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것을 의미한다. 무결성 제약이 모든 계좌는 잔고가 있어야 한다면 이를 위반하는 트랜잭션은 중단된다.
  • 독립성(Isolation)은 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 은행 관리자는 이체 작업을 하는 도중에 쿼리를 실행하더라도 특정 계좌간 이체하는 양 쪽을 볼 수 없다. 공식적으로 고립성은 트랜잭션 실행내역은 연속적이어야 함을 의미한다. 성능관련 이유로 인해 이 특성은 가장 유연성 있는 제약 조건이다. 자세한 내용은 관련 문서를 참조해야 한다.
  • 지속성(Durability)은 성공적으로 수행된 트랜잭션은 영원히 반영되어야 함을 의미한다. 시스템 문제, DB 일관성 체크 등을 하더라도 유지되어야 함을 의미한다. 전형적으로 모든 트랜잭션은 로그로 남고 시스템 장애 발생 전 상태로 되돌릴 수 있다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주될 수 있다.
Elasticsearch

 

Lucene을 기반으로 한 검색 엔진이다.

내가 생각하는 주요 특징으로는 전문검색(full-text), 역색인, 분산처리로 실시간성 빠른 검색 가능,

Schemaless, RESTful API, Multi-tenancy가 있다.

  • 전문 검색
    • 내용 전체를 색인해서 특정 단어가 포함된 문서를 검색할 수 있다.
  • 역색인 
    • 일반적인 색인은 책의 목차를 말하는 것이라면 역색인은 책 뒤편의 단어별 색인 페이지라고 보면 된다. 문서의 위치를 찾아 빠르게 접근하겠다는 목적이 아니라 문서 내의 문자 내용과 같은 내용 물에 빠르게 접근하겠다는 목적에서 나온 개념이다.
  • Schemaless
    • 비정형 문서도 자동으로 색인하고, 검색할 수 있게 한다. 
  • Multi-tenancy
    • 여러 인덱스에 걸쳐 검색이 가능하다 (필드명이 같다는 전제 하에)

 

 

참고 :

https://jaemunbro.medium.com/elastic-search-%EA%B8%B0%EC%B4%88-%EC%8A%A4%ED%84%B0%EB%94%94-ff01870094f0

 

 

String 배열과 ArrayList의 성능 차이에 대한 OKKY의 질문의 답변 중 몇 가지 기억해야 해둬야 할 게 있어서 기록

https://okky.kr/article/266413


댓글 )

이미 답변이 충분히 나왔지만... 몇자 적자면 간단한 자료구조만 공부하셔도 알 수 있는 질문입니다.
배열이던 ArrayList던 자기가 사용할 공간(힙)을 얻을 때가 질문자가 말하는 성능의 포인트입니다.

배열을 사용해 5개만 사용하는 공간에 +1을 하고 싶다면 6개 공간을 만들고 앞에 5개를 복사하고 1 하는 값을 추가합니다. 즉 처음 5개 만드는 공간(성능), 두 번째 6개 만드는 공간입니다.

ArrayList라고 별반 다르지 않습니다. 위의 과정이 add에 들어 있습니다. add 할 때 공간이 부족하면 추가하고 이미 있던 것 복사하고 add 합니다. 그게 기본적으로 10(용량)이라고 초기화가 되어 있고요. 만약 arrayList에 들어갈 최소 크기를 안다면 new ArrayList(사이즈) 이렇게 하시면 입력하면서 메모리를 추가(기본 10) 하는 비용은 좀 줄겠죠... 물론 근데 저렇게까지 한다고 해도 엄청난 양을 넣지 않는 이상 밀리 세컨드도 차이 안 납니다.

그리고 remove 할 땐 그냥 해당 index 없앨 뿐입니다. 실제 할당받은 메모리를 다시 작게 하거나 하진 않습니다. 100000개 넣어 놓고 1개만 남기고 다 지우더라도 메모리 할당받은 건 유지하고 있습니다. 나중에 다시 쓸 수 있으니까요. 뭐 이런 게 많아지면 힙 메모리 없다고 나올 수 도 있겠죠...

아무튼~ 단순 배열로 해결할 수 있다면 그냥 그렇게 하시면 되구요.. 추가가 일어난다 그럼 그냥 잘 만들어진 자료구조 사용하면 됩니다.

자료구조 선택할땐 구조유형을 따져서 조회보단 in/out 자주 일어난다면 위분 말처럼 링크드리스트 쓰구요 반대로 in/out 보다 랜덤엑세스가 많다면 ArrayList쓰면 됩니다. ArrayList자체가 단순 배열과 다른거는 자료구조를 포함하고 있다는것 외에는 동일합니다.


 

정리하자면

 

1. 크기를 안다고 하면 String 배열을 이용해 지정해두면 좋겠지만 그게 성능에 크게 영향을 미치지는 않는다.

2. 배열에 add로 값을 추가하게 되면 새로 정의된 크기의 공간을 힙에 새로 할당해서 기존 값 복사하고 추가하는 것

3. 2번의 이유로 기존에 정의된 크기에서 오버되는 경우에는 성능상에 문제가 생길 수 있다. (디폴트 크기는 10)

4. remove의 경우에는 해당 index를 없앨 뿐이고, 실제 할당받은 메모리는 그대로 유지된다. (메모리 차지는 계속함)

5. 값을 추가하고 제거하는 케이스가 빈번할 경우에는 LinkedList를 쓰는 것이 낫다. 

6. 생성된 List에 단순 접근하고 작업하는 경우에는 ArrayList를 쓰는 것이 낫다.

 

사수님이 다른 사람의 코드도 많이 봐야 된다고 말씀해 주셨는데

백엔드 개발자가 팀에 2명 밖에 없기도 하고, 뭔가 동기부여도 잘 안되고 해서

거의 혼자만의 개발 세계에서 코드를 써내려왔던 것 같다. 

그러다가 이제 또 새로운 개발 프로젝트에 착수하게 되었는데 

뭔가... 이렇게 가다간 발전도 없이 계속 같은 코드만, 같은 기술만 사용할 것 같단 위기감이 들었다.

 

그래서 요즘 컨퍼런스도 좀 챙겨 보고 핫 하다는 기술도 좀 기웃기웃 거리고 있다.

Reactive Kafka가 무엇인지 알기 위해 NHN FORWARD 2019의 세션 영상을 보다가 샘플 코드를 얻게 되었는데

Kafka는 제쳐두고 일단 코드가 굉장히 깔끔하고 구조도 내가 써먹을게 많겠다 싶었다.

이참에 다른 사람의 소스 코드도 좀 봐야지란 생각에 우선 코드 구조부터 파악하기로 했다.

 

카프카에 대한 개념은 이전 포스팅에서 다뤘습니다 (https://modimodi.tistory.com/72)

 

 

https://github.com/EleganceLESS/nhn-forward-2019

 

GitHub - EleganceLESS/nhn-forward-2019

Contribute to EleganceLESS/nhn-forward-2019 development by creating an account on GitHub.

github.com

 

아래 이미지는 Class Diagram이 아니라는 것을 명확히 하고 싶다 ^^;

소스코드 이해를 위해서 파워포인트로 나름 클래스 구조를 그려봤는데 이게 나의 최선이라는 것에 좌절감이ㅋㅋ

 

Kafka는 제쳐두고 우선 참고할 만한 코드 부분에서 인상적이었던 부분을 좀 정리해보도록 하겠다. 

  • 우선 abstract 클래스를 많이 사용한 것이 눈여겨볼 부분이다. 
    • AS-IS 관계를 abstract 클래스를 활용해 더 세밀하게 컨트롤했다.
  • DemoController에는 각 자식 Controller에서 공통적으로 사용하는 /start, /stop EndPoint가 존재한다.
    • DemoController는 DemoService를 주입받고, DemoService의 start(), stop() 메서드를 활용한다.
    • 각 Controller에서는 메시지를 소비하고 정지하는 방법은 같지만 메시지를 생성하는 방법은 다른 Service들을 Injection 시킨다. 
  • DemoService는 추상클래스로 consume()이라는 추상 메서드를 가진다. 
    • DemoService의 다른 메서드는 자식 Service에서 사용되지만 consume() 추상 메서드는 서비스별로 다르기에 이 구조를 만든 것으로 보인다. 
  • DemoService의 consume()을 구현한 클래스 또한 추상 클래스이다. DemoService의 자식 클래스는 OperatorDemoService, SubscriberDemoService이고 각각 consumer(), getSubscriber() 추상 메서드를 포함하고 있다.
    • 메서드 구현을 자식 클래스에 맡기는 데 사용은 부모 클래스에서 한다.
      • 예를 들어 SubscriberDemoService의 getSubscriber() 메서드는 추상 메서드이고, 해당 클래스의 cosume() 메서드에서 이 getSubscriber() 메서드가 사용되는데 getSubscriber() 메서드 구현은 자식이 담당한다.

 

각 클래스 별로 공통적으로 사용하는 코드가 있을 때는 AS-IS 관계인지에 따라 상속을 이용했고, AS-IS 관계가 아닌 경우 interface의 default 기능을 활용했다. 그리고 부모-자식 관계를 1 depth 이상으로 가져가려면 그 관계에 대해 치밀하게 생각해봤다는 것이겠지..? 일단 코드 작성 전에 클래스 간의 관계를 고려해 설계부터 잘 하고 개발에 착수해야 겠단 생각이 많이 들었던 코드이다. 

 

테스트 코드도 눈여겨 볼게 많다. 

 

Test Code

 

Controller 단

@Test
    public void startAndStopTest() {
        Step0Service service = mock(Step0Service.class);
        when(service.start()).thenReturn(Mono.just("START"));
        when(service.stop()).thenReturn(Mono.just("STOP"));

        Step0Controller controller = new Step0Controller(service);

        WebTestClient testClient = WebTestClient.bindToController(controller)
                .build();

        testClient.get().uri("/step0/start")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("START");

        testClient.get().uri("/step0/start")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("Already Running");

        testClient.get().uri("/step0/stop")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("STOP");

        testClient.get().uri("/step0/stop")
                .exchange()
                .expectBody(String.class)
                .isEqualTo("Not Running Now");
    }

Repository단

@Test
public void notifyTest() {
    StepVerifier.withVirtualTime(() -> repository.notify(Tuples.of(1, "홍길동")))
            .thenAwait(Duration.ofSeconds(3))
            .expectNext(Tuples.of("홍길동", true))
            .verifyComplete();
}
@Test
public void getReceiversTest() {

    StepVerifier.create(repository.getReceivers(1))
            .verifyComplete();

    StepVerifier.create(repository.getReceivers(2))
            .expectNext(Tuples.of(2, "조조"))
            .verifyComplete();

    StepVerifier.create(repository.getReceivers(3))
            .expectNext(Tuples.of(3, "유비"))
            .verifyComplete();

    StepVerifier.create(repository.getReceivers(4))
            .expectNext(Tuples.of(4, "조조"))
            .expectNext(Tuples.of(4, "손권"))
            .verifyComplete();
}

Service단

@Test
public void step2ConsumerTest() {
    ReceiverOffset offset = mock(ReceiverOffset.class);
    doNothing().when(offset).acknowledge();

    ReceiverRecord<String, String> record1 = mock(ReceiverRecord.class);
    when(record1.key()).thenReturn("1");
    when(record1.value()).thenReturn("1");
    when(record1.receiverOffset()).thenReturn(offset);

    ReceiverRecord<String, String> record2 = mock(ReceiverRecord.class);
    when(record2.key()).thenReturn("2");
    when(record2.value()).thenReturn("2");
    when(record2.receiverOffset()).thenReturn(offset);
}
 @Test
public void samplingTest() {
    Step2Service service = new Step2Service(null, null);

    StepVerifier.withVirtualTime(() -> Flux.just(1, 1, 1, 1, 1)
            .groupBy(Function.identity())
            .flatMap(service::sampling))
            .thenAwait(Duration.ofSeconds(5))
            .expectNext(1)
            .verifyComplete();
}

 

이 프로젝트 코드를 분석하기로 마음 먹은 것이 테스트 코드를 보고 난 다음이다.

mock을 활용해 단위 테스트를 진행하고, StepVerifier나 doNothing() ..? 이건 내가 한 번도 사용해보지 않은;

이미 작성해 놓은 테스트 코드를 다 뜯어 고치고 싶어서 시간 될 때 마다 조금씩 뜯어 고치고는 있는데

이게 한 두개 건드리다 보니 정말 끝도 없다^^;;

사수님이 말씀해주셨듯 이미 작성된 코드 건들다 보면 한도 끝도 없으니

새로 개발할 것들에 새로 익힌 기술들을 사용해보라는 말씀을 명심해야겠다... 

 

 

 

+ Recent posts