Develop/git-github

git 종종 사용하지만 까먹는 명령어들 | git commands I occasionally use but keep forgetting

+ 조금씩 추가할 예정이미 commit한 메세지 author 변경git rebase -i HEAD~N # 원하는 수정 커밋 범위 설정# 원하는 커밋 pick > e 로 수정 후git commit --amend --author="jyami-kim " # --amend로 author 변경git rebase --continue # 다음 rebase 진행 branch upstream 변경local branch가 remote branch를 추적하도록 git branch --set-upstream-to=origin/feature/TM-5644 feature/TM-5644 tag 추가git tag tag-namegit push origin tag-name git local 설정git config --local u..

git 종종 사용하지만 까먹는 명령어들 | git commands I occasionally use but keep forgetting

728x90

+ 조금씩 추가할 예정

이미 commit한 메세지 author 변경

git rebase -i HEAD~N # 원하는 수정 커밋 범위 설정

# 원하는 커밋 pick > e 로 수정 후

git commit --amend --author="jyami-kim <mor2222@naver.com>"   # --amend로 author 변경

git rebase --continue # 다음 rebase 진행

 

branch upstream 변경

local branch가 remote branch를 추적하도록

 git branch --set-upstream-to=origin/feature/TM-5644 feature/TM-5644

 

tag 추가

git tag tag-name
git push origin tag-name

 

git local 설정

git config --local user.email "mor2222@naver.com"
git config --local user.name "jyami-kim"

+ Will be updated gradually

Changing the author of an already committed message

git rebase -i HEAD~N # 원하는 수정 커밋 범위 설정

# 원하는 커밋 pick > e 로 수정 후

git commit --amend --author="jyami-kim <mor2222@naver.com>"   # --amend로 author 변경

git rebase --continue # 다음 rebase 진행

 

Changing branch upstream

Make a local branch track a remote branch

 git branch --set-upstream-to=origin/feature/TM-5644 feature/TM-5644

 

Adding a tag

git tag tag-name
git push origin tag-name

 

Git local configuration

git config --local user.email "mor2222@naver.com"
git config --local user.name "jyami-kim"

댓글

Comments

Develop/DevOps

규모 확장 시스템 설계 기본 | Fundamentals of Designing Systems for Scale

1. 단일 서버 DNS : 도메인 이름을 이용해 웹사이트에 접속한다. DNS에 질의하여 IP로 변환하는 과정이 필요하다. http 요청을 보내고 클라이언트는 응답을 받는다2. 데이터베이스 데이터베이스 : 웹/모바일 트래픽 처리 서버 (웹 계층)와 데이터베이스 서버 (데이터 계층) 분리를 시도한다.3. 로드밸런서 로드밸런서 : 부하 분산 집합에 속한 웹서버들에게 트래픽 부하를 고르게 분산한다 데이터베이스 : 다중화로 성능과 안정성을 보장한다 (master는 쓰기, slave는 읽기)4. 캐시 캐시 : 캐시를 이용해 서버의 요청이 보다 빨리 처리될 수 있게 한다. SOPF가 되지 않게 분산한다5. 콘텐츠 전송 네트워크 (CDN) CND : 정적 콘텐츠(이미지, 비디오, CSS, ..

규모 확장 시스템 설계 기본 | Fundamentals of Designing Systems for Scale

728x90

1. 단일 서버 
   DNS : 도메인 이름을 이용해 웹사이트에 접속한다. DNS에 질의하여 IP로 변환하는 과정이 필요하다. 
   http 요청을 보내고 클라이언트는 응답을 받는다

2. 데이터베이스
   데이터베이스 : 웹/모바일 트래픽 처리 서버 (웹 계층)와 데이터베이스 서버 (데이터 계층) 분리를 시도한다.

3. 로드밸런서
   로드밸런서 : 부하 분산 집합에 속한 웹서버들에게 트래픽 부하를 고르게 분산한다
   데이터베이스 : 다중화로 성능과 안정성을 보장한다 (master는 쓰기, slave는 읽기)

4. 캐시
   캐시 : 캐시를 이용해 서버의 요청이 보다 빨리 처리될 수 있게 한다. SOPF가 되지 않게 분산한다

5. 콘텐츠 전송 네트워크 (CDN)
   CND : 정적 콘텐츠(이미지, 비디오, CSS, JavaScript)는 웹 서버대신 CDN으로 성능을 보장한다

6. 무상태(stateless) 웹 계층
   웹서버 : 무상태 웹 계층을 갖게 함으로써 자동 규모 확장(autoScaling)이 가능하다
   공유 저장소 : 웹서버를 무상태 웹 계층으로 전환하면서, 필요한 상태정보들은 공유 저장소에 저장한다.

7. 데이터 센터
   로드밸런서 : 데이터 센터를 이용해 가용성을 높이고, 전 세계 어디서도 쾌적하게 사용이 가능하다. 지리적 라우팅을 이용하여 사용자의 위치에 따라 가장 가까운 위치의 데이터 센터로 안내한다.

8. 메시지 큐
   메시지 큐 : 서버간 결합을 느슨하게 하여 (loosely coupled) 규모 확장성이 보장되는 안정된 애플리케이션 구성이 가능하게 한다.
   
9. 로그, 메트릭 그리고 자동화
   도구 : 로그, 모니터링, 메트링, 자동화는 규모가 큰 서비스 관리에 용이하다

10. 데이터베이스의 규모 확장
    데이터베이스 : 샤딩으로(수평적 확장 = 서버증설) DB부하를 줄인다.

관련 책 : 가상 면접 사례로 배우는 대규모 시스템 설계 기초


   

1. Single Server 
   DNS : You access a website using a domain name. A process of querying DNS to resolve it into an IP address is needed. 
   You send an HTTP request and the client receives a response.

2. Database
   Database : We separate the server that handles web/mobile traffic (web tier) from the database server (data tier).

3. Load Balancer
   Load Balancer : Evenly distributes traffic load across web servers in the load-balanced set.
   Database : Replication ensures performance and reliability (master handles writes, slave handles reads).

4. Cache
   Cache : Uses cache so that server requests can be processed faster. Distribute caches to avoid becoming a SPOF.

5. Content Delivery Network (CDN)
   CDN : Static content (images, videos, CSS, JavaScript) is served through a CDN instead of web servers to ensure performance.

6. Stateless Web Tier
   Web Server : By making the web tier stateless, auto-scaling becomes possible.
   Shared Storage : When converting web servers to a stateless web tier, the necessary state information is stored in shared storage.

7. Data Centers
   Load Balancer : Using data centers improves availability and enables a smooth experience from anywhere in the world. GeoDNS routing directs users to the nearest data center based on their location.

8. Message Queue
   Message Queue : By loosely coupling servers, it enables building stable applications with guaranteed scalability.
   
9. Logging, Metrics, and Automation
   Tools : Logging, monitoring, metrics, and automation make it easier to manage large-scale services.

10. Database Scaling
    Database : Sharding (horizontal scaling = adding more servers) reduces the database load.

Related Book: System Design Interview – An Insider's Guide


   

댓글

Comments

Develop/DevOps

[Lettuce.io] 4.4 Reactive API 번역본

lettuce.io/core/release/reference/index.html#reactive-api Lettuce Reference GuideConnections to a Redis Standalone, Sentinel, or Cluster require a specification of the connection details. The unified form is RedisURI. You can provide the database, password and timeouts within the RedisURI. You have following possibilities to create a Rlettuce.io이 챕터의 목표 : Reacitve Stream 패턴의 이해와 reactive applica..

[Lettuce.io] 4.4 Reactive API 번역본

728x90

lettuce.io/core/release/reference/index.html#reactive-api

 

Lettuce Reference Guide

Connections to a Redis Standalone, Sentinel, or Cluster require a specification of the connection details. The unified form is RedisURI. You can provide the database, password and timeouts within the RedisURI. You have following possibilities to create a R

lettuce.io


이 챕터의 목표 : Reacitve Stream 패턴의 이해와 reactive application의 설계 방법의 전반적인 이해를 위함.

4.4.1 Motivation

비동기(Asynchronous)와 리액티브 방법론 (reactive methodolgies)는 네트워크나 디스크 IO로 인한 쓰레드의 대기시간 낭비 대신 시스템의 리소스를 좀 더 효율적으로 사용하도록 해준다. 

이런 스타일의 프로그래밍을 용이하게 하기위한 광법위한 기술이 존재하는데, java.util.concurrent.Future 부터 Akka와 같이 완전한 라이브러리들이 있다 (?)

 Project Reactor는 매우 방대한 셋의 asynchronous workflow를 위한 연산자들이 있고, 이것들은 더이상 프레임워크에 의존하지 않으며 매우 많은 Reactive Streams model을 지원한다.

4.4.2 Understanding Reactive Streams

Reactive Stream은 처음에는 표준의 non-blocking back pressure(이게 뭐지) 을 기반으로 한 asynchronous stream의 표준을 제공하기 위해 만들어졌다. 여기에는 런타임 환경 (JVM and Javascript)뿐만 아니라 네트워크 프로토콜도 포함하려하는 목표가 있었다.

Reactive Streams의 범위는 목표를 달성하는 데 필요한 작업이나 엔터티를 설명하는 최소한의 인터페이스, 메서드 및 프로토콜 집합을 찾는 것이다. 그 목표는 non-blocking back pressure 이 있는 asynchronous streams data이다. (?)

그것은 어플리케이션 코드에서 라이브러리를 연결할 필요 없이, 상호자용을 할 수 있도록 허용해주는 multiple reactive composition 라이브러리들 사이의 운영 표준이다.

Reactive Stream의 통합은 주로 Publicsher<T>Subscriber<T> 타입으로 복잡성을 숨기는 composition library (컴포지션 라이브러리)로 제공된다. Lettuce는 publisher를 Mono, Flux로 사용하는 Project Reactor를 사용한다.

Reacitve Stream에 대한 더 자세한 설명 : http://reactive-streams.org. 

4.4.3. Understanding Publishers

Aynchronouse processing은 IO작업이나 계산과 같은 작업을 호추한 스레드에서 분리한다. handle이 그 return 타입이며, 주로 java.util.concurrent.Future와 동일하거나 비슷하다. 유사하게 single object나 collection, exception을 return 한다. asynchronously하게 fetch 한 결과를 가져온 결과를 기억하는 것은 주로 한 프로우의 끝이 아니다. 한번 데이터가 확보되면 항상 혹은 조건부로 추가 요청을 발행할 수 있다. Java 8이나 Promise 패턴을 사용하면 futuers의 chaining은 연속적으로 그 이후의 asynchronous한 request가 발생하도록 할 수 있다. 한번 조건처리가 필요하면 asynchronous한 flow을 interrupted 시키고 synchronized 해야한다. 이런 접근 방식은 가능하지만 asynchronous의 장점을 완전히 활용하지는 않는다.

반면 Publisher<T> 객체는 다양하고 asynchronous한 질문에 다른방향으로 대답한다 : Pull pattern을 Push patter으로 변환한다.

push : 데이터 변경시 변경이 발새된 곳에서 데이터를 보내주는 방식 (Mono, Flux)
pull : 변경된 데이터가 있는지 질의 후 가져오는 방식 (클라이언트 요청 -> 서버 -> 응답)

Publisher<T>는 Futures처럼 single scalar value에 대한 emission 뿐만 아니라 emission sequences 심지어 infinite streams도 지원한다. 당신이 stream 작업을 시작하기만 하면 이 사실을 감사하게 여길 것이다. Project Reactor는 두개의 타입의 publisher가 사용된다 : Mono 와 Flux

Mono : 0부터 1까지의 event를 emit 한다.
Flux : 0
부터 N까지의 event를 emit 한다.

Publisher<T> 는 concurrency(동시성)나 asynchronicity(비동기성)의 특정 소스나 코드가 실행되는 방식에 편향되지 않는다. synchronous, asynchronous 둘다 ThreadPool 내에서 실행된다. Publisher<T>의 consumer는 실제 구현을 supplier에게 맡겨 나중에 supplier의 코드를 수정하지 않고도 변경이 가능하다.

Publisher<T>의 마지막 키포인트는 Publisher<T>를 가져올 때 처리가 되는 것이 아니라, Publisher<T>에 대한 observer가 subscribe하거나 신호가 보내지는 순간 프로세스가 실행된다는 것이다. 이것은 java.util.concurrent.Future와의 중요한 차이점이다. (Future는 created/obtained 가되는 순간 프로세스가 시작되기 때문이다.) 따라서 어떤 옵저버가 Publisher<T>를 subscribe 하지 않으면 아무일도 일어나지 않는다.

4.4.4. A word on the lettuce Reacitve API

모든 커맨드는 Flux<T>, Mono<T>, Mono<Void> 를 return 한다. 이것들은 Subscriber가 subscribe할 수 있다. 이 구독자는 Publisher <T>가 emit하는 아이템 또는 아이템의 시퀀스에 반응한다. 이 패턴은 Publisher<T>가 객체를 emit 할때까지 기다리는 동안 차단을 할 필요가 없어 동시작업(concurrent operations)에 용이하다. 대신, Publisher<T>가 어떤 futuer time에든 적절하게 반응하도록 준비가 되어있는 Subscriber 형태로 sentry를 만든다.

4.4.5 Consuming Publisher<T>

publisher를 사용할 때 가장먼저 고려할 일은 그들을 consume하는 것이다. Consuming a publisher의 의미는 subscribing을 의미한다. 

    @Test
    @DisplayName("Consuming Publisher 예제 : emit 된 모든 항목을 subscribe 하고 print 한다.")
    void consumingPublisher() {
        Flux.just("jyami", "java", "javabom").subscribe(new Subscriber<String>() {
            @Override
            public void onSubscribe(Subscription s) {
                s.request(3);
            }

            @Override
            public void onNext(String s) {
                System.out.println("Hello " + s + "!");
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("Complete consuming!");
            }
        });
    }
Hello jyami!
Hello java!
Hello javabom!
Complete consuming!

 

모든 구독자 또는 관찰자 (Subscriber or Observer)가 모든 이벤트에 대한 알림을 받고 완료된 이벤트도 받은걸을 확인 할 수 있다.한개의 Publisher<T>는 exception이 발생하거나, 그 Publisher<T>가 종료되었다고 onComplete()를 호출할 때까지 아이템을 내 보낸다. 그 이후에는 더이상 element가 emit되지 않는다.

subscribe의 호출로 한개의 Subscription이 등록되고 이것은 cancel을 허용하므로 더이상 이벤트를 수진하지 않는다. Publisher는 한번 Publisher<T>가 unsubscribed하면 구독 취소및 리소스 할당 해제(free resource)를 상호 운용할 수 있다. (?)

좀더 간단한 포맷의 Subscriber<T> 구현

    @Test
    @DisplayName("Subscriber<T> 의 더 간단한 구현")
    void subscriberSimpleImpl() {
        Flux.just("Ben", "Michael", "Mark").doOnNext(new Consumer<String>() {
            public void accept(String s) {
                System.out.println("Hello " + s + "!");
            }
        }).doOnComplete(new Runnable() {
            public void run() {
                System.out.println("Completed");
            }
        }).subscribe();
    }
    @Test
    @DisplayName("Subscriber<T> 의 더 간단한 구현 - 람다사용")
    void subscriberSimpleImplWithLambda() {
        Flux.just("Ben", "Michael", "Mark")
                .doOnNext(s -> System.out.println("Hello " + s + "!"))
                .doOnComplete(() -> System.out.println("Completed")).subscribe();
    }

Subscriber의 여러 연산자를 이용해서 element를 제어할 수 있다. take() 연산자는 관심이 있는 처음 N개 요소까지로 emit되는 수를 제한한다.

Hello Ben!
Hello Michael!

왜 여기서 Completed가 같이 출력이 안되는지를 모르겠다.

take 연산자는 기대하는 요소 수를() emit한 후 Publisher<T>에서 암시적으로 subscription을 취소한다.

Publisher<T> 에 대한 subscription은 다른 FluxSubscriber가 수행 할 수도 있다. 커스텀한 Publisher를 구현하지 않는한 항상 Subscriber를 사용하라.

그리고 항상 error handler를 올바르게 구현하는 것을 권장한다. 특정시점에서 일이 잘못될 수 있다 (스택트레이스가 쌓여서) 완벽하게 구현이 된 subscriber는 이벤트에 반응할 수 있게 onCompleted, onError 메서드를 모두 선언하자

4.4.6. From push to pull

위에서 햇던 예제는 publisher가 blocking, non-blocking execution에 대해서 별다른 의견없이(추가 없이) 설정되어있는 방법을 기술하였다. Flux<T>는 명시적으로 Iterable<T>로 변환하거나 block() 메서드를 이용해서 synchronized(동기화) 할 수 있다. block() 호출은 피하자. block()을 호출하면 애플리케이션에 대한 reactive chain의 모든 non-blocking 적인 장점이 사라진다.

    @Test
    @DisplayName("block() 연산자 : async -> sycn")
    void blockOperator() {
        String last = Flux.just("Ben", "Michael", "Mark").last().block();
        System.out.println(last); // Mark
    }

 

blocking 호출은 publisher의 체인을 synchronize로 사용되게 할 수 있고, 평범하고 잘 알려진 Pull 패턴으로 돌아가는 방법을 찾는다.

    @Test
    @DisplayName("block() 연산자 : async -> sycn")
    void blockOperatorCollection() {
        List<String> list = Flux.just("Ben", "Michael", "Mark").collectList().block();
        System.out.println(list); // [Ben, Michael, Mark]
    }

toList 연산자는 모든 emmited된 요소들을 모으고(collect), 그리고 그 리스트를 BlockingPublisher<T> 타입으로 패스한다.

4.4.7 Creating Flux and Mono using Lettuce

publisher를 설계하는 방법은 많이 있다. 당신은 이미 just(), take(), collectList()를 이미 보았다. Project Reactor documentation 의 참조에 따르면 Flux와 Mono를 만드는데 사용 할 수 있는 더 많은 메서드가 있다.

Lettuce publisher는 초기화나 체이닝 작업에 사용할 수 있다. Lettuce publisher를 사용하면 non-blocking 동작을 확인 할 수 있다. 이는 모든 I/O 및 커맨드 처리가 netty의 EventLoop를 사용해 aynchronously(비동기적)으로 처리되기 때문이다.

Redis에 연결하는건 매우 간단하다.

private RedisStringReactiveCommands<String, String> commands;

    @BeforeEach
    void setUp() {
        RedisClient client = RedisClient.create("redis://localhost");
        commands = client.connect().reactive();
    }

key에서 value를 가져오려면 GET 연산이 필요하다. (subscribe자리에 Consumer를 넣었다.)

 

    @Test
    @DisplayName("lettuce publisher를 사용하고, 여기서 get 연산자를 통해 redis의 key에 따른 value를 가져올 수 있다. ")
    void LettucePublisher() {
        commands.get("key")
                .subscribe(System.out::println);
    }

이것의 실행은 asynchronously(비동기적으로) 처리되며, Netty EventLoop Thread에서 작업이 완료되는 동안 호출하는 스레드(invoking thread)는 프로세스내의 다른 일을 처리할 수 있다. 분리된 특성으로 인해 호출한 메서드(calling method)는 Publisher<T>의 실행이 완료되기 전에 그대로 둘 수 있다 (?).

Lettuce의 publichser는 연결된 컨텍스트(context of chaining) 내에서 여러개의 키들을 asynchronously(비동기적으로) 로드하는데 사용될 수 있다.

댓글

Comments

Develop/Springboot

QueryDSL 슬라이싱 테스트(@DataJpaTest) / Test Bean 등록 | QueryDSL Slicing Test (@DataJpaTest) / Registering Test Beans

QueryDSL 슬라이싱 테스트 방법에 대해 기록을 남긴다.내가 주로 사용하는 Querydsl 패턴은 CustomRepository를 만드는 방식이다.docs.spring.io/spring-data/jpa/docs/2.1.3.RELEASE/reference/html/#repositories.custom-implementations Spring Data JPA - Reference DocumentationExample 100. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastn..

QueryDSL 슬라이싱 테스트(@DataJpaTest) / Test Bean 등록 | QueryDSL Slicing Test (@DataJpaTest) / Registering Test Beans

728x90

QueryDSL 슬라이싱 테스트 방법에 대해 기록을 남긴다.

내가 주로 사용하는 Querydsl 패턴은 CustomRepository를 만드는 방식이다.

docs.spring.io/spring-data/jpa/docs/2.1.3.RELEASE/reference/html/#repositories.custom-implementations

 

Spring Data JPA - Reference Documentation

Example 100. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v

docs.spring.io

코드는 아래와 같다.

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    Optional<User> findByEmail(String email);
}
public interface UserRepositoryCustom {
    Optional<User> findByUserId(Long userId);
}
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Optional<User> findByUserId(Long userId) {
        User user = jpaQueryFactory.selectFrom(QUser.user)
                .where(QUser.user.userId.eq(userId))
                .fetchOne();
        return Optional.ofNullable(user);
    }
}

 

이렇게 짠 Querydsl 코드를 테스트하기 위한 테스트코드를 짤 때 @SpringBootTest를 하면 모든 빈이 주입되기 때문에 상관이 없지만,

아래 코드와 같이 DataJpaTest와 같은 슬라이싱 테스트를 하고싶을 때 문제가 발생한다.

@DataJpaTest
@ActiveProfiles("test")
class UserRepositoryTest {

    @Autowired
    private EntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    private User settingUser() {
        User settingUser = User.builder()
                .email("jyami@ewhain.net")
                .name("jyami")
                .build();

        return userRepository.save(settingUser);
    }

    @Test
    void test() {
        settingUser();
        entityManager.clear();
        User user = userRepository.findByUserId(1L)
                .orElseThrow(() -> new ResourceNotFoundException("user", "userId", 1L));
    }



}
Error creating bean with name 'userRepositoryImpl' defined in file [/Users/jyami/Documents/commiters/commiters-ewha/commiters-ewha-api/build/classes/java/main/com/jyami/commitersewha/domain/user/UserRepositoryImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory'

결국 이유는 JpaQueryFactory가 persistenceLayer가 아니어서 빈등록이 되지않아 발생하는 문제인데, 
이때 테스트 시 특정부분의 빈만 등록해주는 방법이 있었다!

@TestConfiguration
public class TestConfig {

    @PersistenceContext
    private EntityManager entityManager;

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

테스트에서만 사용할 용도의 @TestConfiguration을 이용해 JPAQueryFactory 만 Bean으로 생성해준다.

@DataJpaTest
@ActiveProfiles("test")
@Import(TestConfig.class)
public class UserRepositoryTest {
}

 

이후 @Import 어노테이션을 사용해 해당 테스트용 빈을 주입해주면, JpaQueryFactory에 대한 빈도 생성되므로, Querydsl의 슬라이싱 테스트가 가능해진다 :)


도움: github.com/jojoldu/blog-comments/issues/277#issuecomment-702597745

 

I'm leaving a note on how to do slicing tests with QueryDSL.

The Querydsl pattern I mainly use involves creating a CustomRepository.

docs.spring.io/spring-data/jpa/docs/2.1.3.RELEASE/reference/html/#repositories.custom-implementations

 

Spring Data JPA - Reference Documentation

Example 100. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v

docs.spring.io

The code looks like this.

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    Optional<User> findByEmail(String email);
}
public interface UserRepositoryCustom {
    Optional<User> findByUserId(Long userId);
}
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Optional<User> findByUserId(Long userId) {
        User user = jpaQueryFactory.selectFrom(QUser.user)
                .where(QUser.user.userId.eq(userId))
                .fetchOne();
        return Optional.ofNullable(user);
    }
}

 

When writing test code for Querydsl code like this, using @SpringBootTest would be fine since all beans get injected,

but a problem occurs when you want to do a slicing test like @DataJpaTest as shown in the code below.

@DataJpaTest
@ActiveProfiles("test")
class UserRepositoryTest {

    @Autowired
    private EntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    private User settingUser() {
        User settingUser = User.builder()
                .email("jyami@ewhain.net")
                .name("jyami")
                .build();

        return userRepository.save(settingUser);
    }

    @Test
    void test() {
        settingUser();
        entityManager.clear();
        User user = userRepository.findByUserId(1L)
                .orElseThrow(() -> new ResourceNotFoundException("user", "userId", 1L));
    }



}
Error creating bean with name 'userRepositoryImpl' defined in file [/Users/jyami/Documents/commiters/commiters-ewha/commiters-ewha-api/build/classes/java/main/com/jyami/commitersewha/domain/user/UserRepositoryImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory'

The reason is that JpaQueryFactory isn't part of the persistence layer, so it doesn't get registered as a bean.
But there's a way to register only specific beans during testing!

@TestConfiguration
public class TestConfig {

    @PersistenceContext
    private EntityManager entityManager;

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

Using @TestConfiguration, which is meant only for tests, we create just the JPAQueryFactory as a Bean.

@DataJpaTest
@ActiveProfiles("test")
@Import(TestConfig.class)
public class UserRepositoryTest {
}

 

Then, by using the @Import annotation to inject the test-specific bean, the JpaQueryFactory bean also gets created, making slicing tests with Querydsl possible :)


Credit: github.com/jojoldu/blog-comments/issues/277#issuecomment-702597745

 

댓글

Comments

Develop/Springboot

Spring Security OAuth2 Login Flow | Spring Security OAuth2 Login Flow

www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/ Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 2Integrate social login with Facebook, Google, and Github in your spring boot application using Spring Security's OAuth2 functionalities. You'll also add email and password based login along with social login.www.callicoder.com에 있는 글을 번역하여, 공부하는 용도 입니다. OAu..

Spring Security OAuth2 Login Flow | Spring Security OAuth2 Login Flow

728x90

www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/

 

Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 2

Integrate social login with Facebook, Google, and Github in your spring boot application using Spring Security's OAuth2 functionalities. You'll also add email and password based login along with social login.

www.callicoder.com

에 있는 글을 번역하여, 공부하는 용도 입니다.

 

OAuth2 Login Flow 

SecurityConfig.java 파일과 관련한 OAuth2 LoginFlow. 글로 설명된 흐름을 그림으로 그려보았다.

1. OAuth2 login 플로우는 맨처음 frontend client 에서 엔드포인트에 서 요청을 보내면서 시작된다.

http://localhost:8080/oauth2/authorize/{provider}?redirect_uri=<redirect_uri_after_login>

  • provider : google, facebook, github
  • redirect_uri : OAuth2 provider가 성공적으로 인증을 완료했을 때 redirect 할 URI를 지정한다. (OAuth2의 redirectUri 와는 다르다)

2. endpoint로 인증 요청을 받으면, Spring Security의 OAuth2 클라이언트는 user를 provider가 제공하는 AuthorizationUrl로 redirect 한다.
Authorization request와 관련된 state는 authorizationRequestRepository 에 저장된다 (Security Config에 정의함)
provider에서 제공한 AutorizationUrl에서 허용/거부가 정해진다.

  • 이때 만약 유저가 앱에 대한 권한을 모두 허용하면 provider는 사용자를 callback url로 redirect한다. (http://localhost:8080/oauth2/callback/{provider}) 그리고 이때 사용자 인증코드 (authroization code) 도 함께 갖고있다.
  • 만약 거부하면 callbackUrl로 똑같이 redirect 하지만 error가 발생한다.

3. Oauth2 에서의 콜백 결과가 에러이면 Spring Security는 oAuth2AuthenticationFailureHanlder 를 호출한다. (Security Config에 정의함)

4. Oauth2 에서의 콜백 결과가 성공이고 사용자 인증코드 (authorization code)도 포함하고 있다면 Spring Security는 access_token 에 대한 authroization code를 교환하고, customOAuth2UserService 를 호출한다 (Security Config에 정의함)

5. customOAuth2UserService 는 인증된 사용자의 세부사항을 검색한 후에 데이터베이스에 Create를 하거나 동일 Email로 Update 하는 로직을 작성한다.

6. 마지막으로 oAuth2AuthenticationSuccessHandler 이 불리고 그것이 JWT authentication token을 만들고 queryString에서의 redirect_uri로 간다 (1번에서 client가 정의한 ) 이때 JWT token과 함께!

 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Autowired
    private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

    @Autowired
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    
    @Autowired
    private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

    /*
      By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
      the authorization request. But, since our service is stateless, we can't save it in
      the session. We'll save the request in a Base64 encoded cookie instead.
    */
    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf()
                    .disable()
                .formLogin()
                    .disable()
                .httpBasic()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                    .and()
                .authorizeRequests()
                    .antMatchers("/",
                        "/error",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                        .permitAll()
                    .antMatchers("/auth/**", "/oauth2/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated()
                    .and()
                .oauth2Login()
                    .authorizationEndpoint()
                        .baseUri("/oauth2/authorize")
                        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                        .and()
                    .redirectionEndpoint()
                        .baseUri("/oauth2/callback/*")
                        .and()
                    .userInfoEndpoint()
                        .userService(customOAuth2UserService)
                        .and()
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .failureHandler(oAuth2AuthenticationFailureHandler);

        // Add our custom Token based authentication filter
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/

 

Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 2

Integrate social login with Facebook, Google, and Github in your spring boot application using Spring Security's OAuth2 functionalities. You'll also add email and password based login along with social login.

www.callicoder.com

This is a translation of the article above, for study purposes.

 

OAuth2 Login Flow 

The OAuth2 Login Flow related to the SecurityConfig.java file. I drew a diagram based on the flow described in the text.

1. The OAuth2 login flow begins when the frontend client sends a request to the following endpoint.

http://localhost:8080/oauth2/authorize/{provider}?redirect_uri=<redirect_uri_after_login>

  • provider: google, facebook, github
  • redirect_uri: Specifies the URI to redirect to when the OAuth2 provider successfully completes authentication. (This is different from OAuth2's redirectUri)

2. When the authentication request is received at the endpoint, Spring Security's OAuth2 client redirects the user to the AuthorizationUrl provided by the provider.
The state related to the authorization request is stored in the authorizationRequestRepository (defined in Security Config).
The allow/deny decision is made at the AuthorizationUrl provided by the provider.

  • If the user grants all permissions to the app, the provider redirects the user to the callback URL (http://localhost:8080/oauth2/callback/{provider}) along with the user's authorization code.
  • If the user denies, they are redirected to the same callbackUrl, but with an error.

3. If the OAuth2 callback result is an error, Spring Security invokes the oAuth2AuthenticationFailureHanlder (defined in Security Config).

4. If the OAuth2 callback result is successful and includes the authorization code, Spring Security exchanges the authorization code for an access_token and invokes the customOAuth2UserService (defined in Security Config).

5. The customOAuth2UserService retrieves the authenticated user's details and then either creates a new entry in the database or updates the existing one with the same email.

6. Finally, the oAuth2AuthenticationSuccessHandler is called, which creates a JWT authentication token and redirects to the redirect_uri from the queryString (the one defined by the client in step 1) — along with the JWT token!

 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Autowired
    private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

    @Autowired
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    
    @Autowired
    private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

    /*
      By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
      the authorization request. But, since our service is stateless, we can't save it in
      the session. We'll save the request in a Base64 encoded cookie instead.
    */
    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf()
                    .disable()
                .formLogin()
                    .disable()
                .httpBasic()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                    .and()
                .authorizeRequests()
                    .antMatchers("/",
                        "/error",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                        .permitAll()
                    .antMatchers("/auth/**", "/oauth2/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated()
                    .and()
                .oauth2Login()
                    .authorizationEndpoint()
                        .baseUri("/oauth2/authorize")
                        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                        .and()
                    .redirectionEndpoint()
                        .baseUri("/oauth2/callback/*")
                        .and()
                    .userInfoEndpoint()
                        .userService(customOAuth2UserService)
                        .and()
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .failureHandler(oAuth2AuthenticationFailureHandler);

        // Add our custom Token based authentication filter
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

댓글

Comments

Develop/DevOps

GCP MySQL(MariaDB) DB 외부연결 | GCP MySQL (MariaDB) DB External Connection

GCP compute engine에 mysql을 연결하기 위해 삽질하면서 정리한 내용.1. compute engine에 mysql 설치sudo apt-get updatesudo apt-get install mysql-serversudo mysql_secure_installation // mysql 설정mysql_secure_installation에서 설정한 패스워드로 입력해서 접속한다.접속 방법 : root 유저로 접속하고, 패스워드를 입력하겠음sudo mysql -u root -p 2. 외부 ip 접근에 대한 포트 방화벽 열기VPC network > filewallrules 메뉴에 들어가기traffic : ingress protocols and port : 33063306 포트가 방화벽에 연결된다.소스..

GCP MySQL(MariaDB) DB 외부연결 | GCP MySQL (MariaDB) DB External Connection

728x90

GCP compute engine에 mysql을 연결하기 위해 삽질하면서 정리한 내용.

1. compute engine에 mysql 설치

sudo apt-get update
sudo apt-get install mysql-server
sudo mysql_secure_installation // mysql 설정

mysql_secure_installation에서 설정한 패스워드로 입력해서 접속한다.

접속 방법 : root 유저로 접속하고, 패스워드를 입력하겠음

sudo mysql -u root -p

 

2. 외부 ip 접근에 대한 포트 방화벽 열기

VPC network > filewallrules 메뉴에 들어가기

traffic : ingress

protocols and port : 3306

3306 포트가 방화벽에 연결된다.

소스 필터나 대상은 커스텀하게 사용하자. 난 범용적 사용을 위해 모든 인스턴스로 지정해둠

 

3. demon addresss 변경하기

ip 통해서 mysql에 접근 가능하도록 할 때 mysql 데몬이 0.0.0.0으로 떠 있어야 한다 (기본값 127.0.0.1)
이를 위해서 compute engine의 shell에서 conf 파일 수정이 필요하다.

cd /ect/mysql/mariadb.conf.d
sudo vi 50-server.cnf

해당 경로에 설정파일이 존재한다. 이곳으로 들어가서 bind-address를 변경해준다.

# bind-address = 127.0.0.1
bind-address = 0.0.0.0

이렇게 해준 후에 데몬을 재시작해주어야 한다.

service restart를 해주어도 되지만, vm에 안 깔려 있길래 다른 방법을 사용하였다.

sudo /etc/init.d/mysql restart

 

4. 접속 유저 권한 부여

위에서 sudo 권한을 이용해 db에 접속했었다. 따라서 외부 접속 전용 사용자를 추가하고, 해당 사용자에 권한을 부여 해주자

User 목록 확인

SELECT user, host FROM mysql.user;

 

사용자 계정 추가

CREATE USER 'jyami'@'%' IDENTIFIED BY 'password';
  • 사용자 명 : jyami
  • 접근 : % - 외부에서의 접근을 허용함
  • 비밀번호 : password

사용자 접근 권한

localhost local에서만의 접근을 허용함
% 모든 IP에서의 접근을 허용함
xxx.xxx.xxx.xxx 지정한 IP에서만 접근을 허용함
xxx.xxx.% 특정 IP 대역에서의 접근을 허용함

DB 권한 부여

GRANT ALL PRIVILEGES ON *.* TO 'jyami'@'%' WITH GRANT OPTION;

모든곳에서 접속하는 사용자 jyami에게 모든 권한을 부여한다.

*.* 모든 것을 할 수 잇는 권한
[database_name].* 특정 DB를 관리할 수 있는 권한
[database_name].[tablename] 특정 DB의 특정 Table을 관리할 수 잇는 권한

부여된 권한 보기

SHOW GRANTS FOR jyami@%

 

5. springboot 접속

GCE로 접근했을 때 분명 위 명령어대로 mysql을 깔았는데 직접 Mysql에 접속해보니 MariaDB [DB]> 로 떴었다. 이때 mysql connection은 안됐는데, mariaDB connection은 됐다.

[DB가 MariaDB 인 경우]

# build.gradle
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.1.2'
# application.yml

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

[DB가 MySQL 인 경우]

# build.gradle
runtimeOnly 'mysql:mysql-connector-java'
# application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

6. 추가 DB 관련 명령어

database 목록 확인

SHOW DATABASES;

사용할 database 지정

USE [database 이름];

database 생성

CREATE DATABASE [데이터베이스 이름];

 

이외 외부 접속 관련 참고 사이트

https://m.blog.naver.com/PostView.nhn?blogId=varkiry05&logNo=198610727&proxyReferer=https:%2F%2Fwww.google.com%2F

Notes from my struggles trying to connect MySQL to a GCP compute engine.

1. Installing MySQL on Compute Engine

sudo apt-get update
sudo apt-get install mysql-server
sudo mysql_secure_installation // mysql 설정

Log in using the password you set during mysql_secure_installation.

How to connect: Log in as the root user with a password

sudo mysql -u root -p

 

2. Opening Firewall Ports for External IP Access

Go to VPC network > firewall rules menu

traffic : ingress

protocols and port : 3306

Port 3306 will be opened in the firewall.

Customize the source filters and targets as you need. I set it to all instances for general-purpose use.

 

3. Changing the Daemon Address

To allow MySQL access via IP, the MySQL daemon needs to be listening on 0.0.0.0 (the default is 127.0.0.1)
To do this, you need to modify the conf file in the compute engine's shell.

cd /ect/mysql/mariadb.conf.d
sudo vi 50-server.cnf

The config file is located at this path. Go in and change the bind-address.

# bind-address = 127.0.0.1
bind-address = 0.0.0.0

After making this change, you need to restart the daemon.

You could use service restart, but since it wasn't installed on the VM, I used a different method.

sudo /etc/init.d/mysql restart

 

4. Granting User Permissions

Earlier, we connected to the DB using sudo privileges. So let's add a dedicated user for external access and grant permissions to that user.

Check User List

SELECT user, host FROM mysql.user;

 

Add a User Account

CREATE USER 'jyami'@'%' IDENTIFIED BY 'password';
  • Username: jyami
  • Access: % - allows access from external sources
  • Password: password

User Access Scope

localhost Allows access only from local
% Allows access from all IPs
xxx.xxx.xxx.xxx Allows access only from a specific IP
xxx.xxx.% Allows access from a specific IP range

Grant DB Privileges

GRANT ALL PRIVILEGES ON *.* TO 'jyami'@'%' WITH GRANT OPTION;

This grants all privileges to the user jyami connecting from anywhere.

*.* Privileges to do everything
[database_name].* Privileges to manage a specific DB
[database_name].[tablename] Privileges to manage a specific table in a specific DB

View Granted Privileges

SHOW GRANTS FOR jyami@%

 

5. Connecting from Spring Boot

When I accessed the GCE, I definitely installed MySQL following the commands above, but when I actually connected to MySQL, it showed MariaDB [DB]>. In this case, the MySQL connection didn't work, but the MariaDB connection did.

[If the DB is MariaDB]

# build.gradle
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.1.2'
# application.yml

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

[If the DB is MySQL]

# build.gradle
runtimeOnly 'mysql:mysql-connector-java'
# application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://[GCE public IP]:3306/[DB명]?useSSL=false&serverTimezone=UTC
    username: [유저이름]
    password: [비밀번호]

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  logging:
    level:
      org:
        hibernate:
          SQL: DEBUG
          type:
            trace

 

6. Additional DB-Related Commands

Check Database List

SHOW DATABASES;

Select a Database to Use

USE [database 이름];

Create a Database

CREATE DATABASE [데이터베이스 이름];

 

Additional reference site for external access

https://m.blog.naver.com/PostView.nhn?blogId=varkiry05&logNo=198610727&proxyReferer=https:%2F%2Fwww.google.com%2F

댓글

Comments

Develop/DevOps

알아두면 언젠간 깨달을 도커지식 2 - 도커 네트워크 | Docker Knowledge You'll Eventually Appreciate 2 - Docker Network

이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다!그리고 항상 도커의 길로 인도해주는 영우찡 감사여! 가상 네트워크아이피의 갯수는 제한적이다 우리가 사용하는 공유기는 주로 192.168.x.x 아이피 대역으로 내 컴퓨터에 아이피를 할당해준다. 그치만 이 아이피는 내 공유기 밖에서는 접근 할 수 없다.그럼 이 때 외부에서 공유기 안의 특정 서버에 접근하기 위해서는 포트 포워딩을 사용한다. 포트포워딩이 필요한 이유는 공유기는 하나인데 비해(ip는 하나인데 비해) 내부 서버는 여러대일 수 있으니 어느 서버의 포트로 연결을 해주어야 하는지 몰라서 포트 포워딩을 사용하는 것이다. 외부에서 8080..

알아두면 언젠간 깨달을 도커지식 2 - 도커 네트워크 | Docker Knowledge You'll Eventually Appreciate 2 - Docker Network

728x90

이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다
필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다!
그리고 항상 도커의 길로 인도해주는 영우찡 감사여!

 

가상 네트워크

아이피의 갯수는 제한적이다 우리가 사용하는 공유기는 주로 192.168.x.x 아이피 대역으로 내 컴퓨터에 아이피를 할당해준다. 그치만 이 아이피는 내 공유기 밖에서는 접근 할 수 없다.

그럼 이 때 외부에서 공유기 안의 특정 서버에 접근하기 위해서는 포트 포워딩을 사용한다. 포트포워딩이 필요한 이유는 공유기는 하나인데 비해(ip는 하나인데 비해) 내부 서버는 여러대일 수 있으니 어느 서버의 포트로 연결을 해주어야 하는지 몰라서 포트 포워딩을 사용하는 것이다. 
외부에서 8080포트를 요청할 때 공유기 하위에 있는 서버들이 모두 8080을 쓰고 있을 때 어느 서버의 8080인지를 몰라서 서버를 지정하는게 목적이다. 추가로 서버를 지정하면서 포트도 바꿀수 있게 되었고!

주로 사용하는 http와 tcp 요청은 ip와 포트로 타겟을 지정하는데, 외부에서 요청할 때

ip는 우리집 공유기가 가진 공인 아이피를 지정할 테고,
port는 공유기 아래 있는 여러대의 서버중에 어떤 곳에 이 요청을 전달해야 할 지 모르기 때문에 포트포워딩으로 미리 정해두고 전달하는 것이다.

 

도커의 포트포워딩

도커도 마찬가지로 -p 플래그를 사용해서 포트포워딩을 지정할 수 있다. 도커도 하나의 서버에 여러개의 컨테이너가 있어서, 서버의 포트에 컨테이너를 지정할지가 애매해서, 위에서 공유기에서 서버를 지정할 때 애매했던 것과 동일한 이유로 포트포워딩이 필요하다.

도커환경에서는 OS가 설치된 host machine이 공유기의 역할을 하고, 도커가 가상 망을 이루고 있는 것이다.

ip는 도커가 설치된 머신을 의미하고
port는 도커 망안에서 어떤 컨테이너로 연결을 할지에 대한 의미로 생각하자 (-p)

 

도커 안에서는 모든 컨테이너가 전부 다른 서브넷을 가진다.

도커 컨테이너 끼리는 통신을 하지 못한다
-> 컨테이너끼리 연결해주는 작업이 도커 안에 네트워크를 다는 것

사실 컨테이너 가상화가 각자 분리된 영역을 만들어 주기 위함이었으니 서로 통신도 분리를 해둔 것이라고 생각하면 편하다.(필요할 경우에 통신을 연결하면 되도록 만든 것)

 

도커 네트워크

하지만 컨테이너가 분리가 되어있다 하더라도 통신은 필요하다. 예를 들자면 내 springboot 서비스에 대한 컨테이너 A와 DB에 대한 컨테이너 B 둘사이에서 통신이 일어나는 경우를 생각할 수 있을 것이다.

따라서 컨테이너끼리 통신을 위해 도커 네트워크를 만들어서 여러개의 컨테이너를 하나의 가상의 망 아래에 묶을 수 있다.

이런식으로 네트워크를 만들고

docker network create test_network

만들어진 네트워크에 컨테이너를 등록하는 형태로 지정하면 컨테이너간 통신이 가능해진다.

docker network connect web_server_container

 

이때 문제는 컨테이너의 아이피가 랜덤으로 만들어진다는 점이다. (아이피가 컨테이너마다 부여된다) 컨테이너가 뜰 때마다 아이피가 랜덤이다.

아이피가 랜덤으로 부여되면, 아이피를 이용해서 통신을 할 수 없다. 따라서 통신을 하기위해서는 컨테이너 네임으로 지정하는 방식을 이용한다.

컨테이너 네임이 Container network interface(CNI)이라는 도커 내부의 가상망을 만들어주는 곳에 등록되어 있어 ip 대신 컨테이너 네임을 DNS 서버에 등록한다.

도커 네트워크에 컨테이너가 등록될 때 컨테이너의 ip는 랜덤하게 부여되지만, CNI 덕분에 컨테이너 네임을 이용할 수 있다.

결국 도커에서 네트워크 통신을 할 때 DNS 검색 우선 순위에서 CNI가 1순위이기 때문에 컨테이너 이름으로 호출이 가능한 것이다.

컨테이너 이름을 아이피 대신 사용한다.

 

그래서 웹앱 컨테이너 하나랑 db 컨테이너하나 띄우고 두 컨테이너를 하나의 네트워크로 묶어서 앱에서 DB를 호출할 때.

  네임스페이스 포트
웹앱 컨테이너 web_app_container 9090
DB 컨테이너 DB_container 3306

다음과 같은 정보를 갖고있다면, 그저 springboot 에서 DB 컨테이너에 연결을 하고자 한다면 DB_container:3306 이런식으로 적어도 작동이 된다는 것이다.

 

이해를 위한 예제

Q. 집에 공유기가 있고, 그 안에 서버가 있고, 그안에 컨테이너 한경에서 9090 포트로 서비스하는앱이 있다. 이때 포트 포워딩은 몇 번 일어날까?

A. 2번일어난다 : 공유기에서 한번, 도커에서 한번

Q. 클라우드 환경에서 nginx를 사용한 포트포워딩을 할 때, nginx는 springboot 로 연결 연결, springboot에서는 db로 연결할 때 도커 네트워크 통신은 몇번 일어날까?

A. 2번 일어난다 : nginx의 proxy_pass 1번, spring에서 mysql로의 1번

container_name:portNumber 의 규칙을 사용하여 도커 네트워크 통신을 한다

[연결 글]

2020/07/12 - [Develop/DevOps] - 알아두면 언젠간 깨달을 도커지식 1 - 가상화

 

알아두면 언젠간 깨달을 도커지식 1 - 가상화

이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다 필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다! 그리고 항상

jyami.tistory.com

 

This post is written in a rough-and-ready style and gets updated as I learn more
This may reflect a highly subjective perspective gained while studying Docker, so please leave a kind comment if anything seems off!
And thanks as always to Youngwoo for guiding me down the path of Docker!

 

Virtual Network

The number of IP addresses is limited. The router we use typically assigns an IP to our computer in the 192.168.x.x range. However, this IP is not accessible from outside our router.

So, to access a specific server behind the router from the outside, we use port forwarding. The reason port forwarding is needed is that while there's only one router (only one IP), there can be multiple internal servers, and there's no way to know which server's port to route the connection to — that's why we use port forwarding. 
When an external request comes in on port 8080, if all the servers behind the router are using 8080, there's no way to tell which server's 8080 it should go to — so the purpose is to specify the server. As a bonus, you can also change the port while specifying the server!

Commonly used HTTP and TCP requests specify targets using IP and port. When making a request from the outside:

The IP will point to the public IP of our home router,
and since there's no way to know which of the multiple servers behind the router should receive the request, we use port forwarding to predetermine and route it.

 

Port Forwarding in Docker

Docker works the same way — you can specify port forwarding using the -p flag. Since Docker also has multiple containers on a single server, it's ambiguous which container should be assigned to the server's port. For the same reason it was ambiguous when specifying servers on a router, port forwarding is needed.

In a Docker environment, the host machine with the OS installed acts as the router, and Docker forms its own virtual network.

Think of the IP as referring to the machine where Docker is installed,
and the port as indicating which container to connect to within the Docker network (-p).

 

Inside Docker, every container has a completely different subnet.

Docker containers cannot communicate with each other
-> The process of connecting containers is adding a network within Docker

Since container virtualization was designed to create isolated areas in the first place, it makes sense that communication is also separated. (It's built so that you connect communication only when needed.)

 

Docker Network

However, even though containers are isolated, communication is still necessary. For example, you can think of a case where container A running your Spring Boot service needs to communicate with container B running the DB.

So, for inter-container communication, you can create a Docker network to group multiple containers under a single virtual network.

You create a network like this:

docker network create test_network

Then you register containers to the created network, and communication between containers becomes possible.

docker network connect web_server_container

 

The problem here is that container IPs are assigned randomly. (Each container gets its own IP.) Every time a container starts up, the IP is random.

If IPs are assigned randomly, you can't rely on IPs for communication. Therefore, to communicate, we use the container name instead.

The container name is registered in Docker's internal virtual network manager called the Container Network Interface (CNI), and the container name is registered in the DNS server instead of the IP.

When a container is registered to a Docker network, its IP is assigned randomly, but thanks to CNI, you can use the container name instead.

Ultimately, when Docker handles network communication, the CNI has the highest priority in DNS lookup, which is why you can call containers by their name.

Container names are used in place of IPs.

 

So when you spin up one web app container and one DB container, group them under a single network, and the app calls the DB:

  Namespace Port
Web App Container web_app_container 9090
DB Container DB_container 3306

If you have the information above, then to connect from Spring Boot to the DB container, you can simply write DB_container:3306 and it will work.

 

Examples for Understanding

Q. You have a router at home, a server behind it, and inside that server, an app serving on port 9090 in a container environment. How many times does port forwarding occur?

A. It happens 2 times: once at the router, once at Docker

Q. In a cloud environment using nginx for port forwarding, where nginx connects to Spring Boot, and Spring Boot connects to the DB — how many times does Docker network communication occur?

A. It happens 2 times: once for nginx's proxy_pass, once for Spring to MySQL

Docker network communication follows the rule of container_name:portNumber

[Related Posts]

2020/07/12 - [Develop/DevOps] - Docker Knowledge You'll Eventually Appreciate 1 - Virtualization

 

Docker Knowledge You'll Eventually Appreciate 1 - Virtualization

This post is written in a rough-and-ready style and gets updated as I learn more. This may reflect a highly subjective perspective gained while studying Docker, so please leave a kind comment if anything seems off! And thanks as always

jyami.tistory.com

 

댓글

Comments

Develop/DevOps

알아두면 언젠간 깨달을 도커지식 1 - 가상화 | Docker Knowledge You'll Eventually Appreciate 1 - Virtualization

이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다!그리고 항상 도커의 길로 인도해주는 영우찡 감사여! 서버 가상화가상화에는 두가지 종류가 있다.하이퍼 바이저 가상화컨테이너 가상화 하이퍼바이저 가상화하이퍼바이저 기반의 가상화를 사용할 경우에는 내 서버 위에 host OS를 설치하고, 사용할 만큼만 자원을 분할하여 가상머신을 생성한 후에 그 위에 Guest OS를 설치하여 그 위에서 어플리케이션을 사용한다.하지만 하이퍼바이저 기반 가상화의 경우엔 vm마다 자원을 따로따로 분리해서 사용을하는데, 중복된 자원이 있을 경우엔 사실 불필요한 용량(자원)을 소모한다고 볼 수 있다. 컨테이너 가상화따라..

알아두면 언젠간 깨달을 도커지식 1 - 가상화 | Docker Knowledge You'll Eventually Appreciate 1 - Virtualization

728x90

이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다
필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다!
그리고 항상 도커의 길로 인도해주는 영우찡 감사여!

 

서버 가상화

가상화에는 두가지 종류가 있다.

  1. 하이퍼 바이저 가상화
  2. 컨테이너 가상화

 

하이퍼바이저 가상화

하이퍼바이저 기반의 가상화를 사용할 경우에는 내 서버 위에 host OS를 설치하고, 사용할 만큼만 자원을 분할하여 가상머신을 생성한 후에 그 위에 Guest OS를 설치하여 그 위에서 어플리케이션을 사용한다.

하지만 하이퍼바이저 기반 가상화의 경우엔 vm마다 자원을 따로따로 분리해서 사용을하는데, 중복된 자원이 있을 경우엔 사실 불필요한 용량(자원)을 소모한다고 볼 수 있다.

 

컨테이너 가상화

따라서 이 OS 기반 가상화 기술을 대체할 수 있게 나온것이 Container 기술이다 이 컨테이너 기술은 LXC(linux Containers)라고 부르며 이 LXC 기술을 잘 활용해서 만든 것이 Docker이다.

 

도커환경에서는 위 그림처럼 Guest OS가 필요하지 않다. 쉬운 이해를 위해 예를 들어보자

 

Host OS가 Cent OS이고, Ubuntu에서 application을 실행하고 싶다.

1. 하이퍼 바이저에서는, GuestOS인 Ubuntu를 설치하고 그 위에서 app을 실행한다. 그러면 Guest OS에 있는 Kernel이 하이퍼 바이저에 명령을 전달하고 하이퍼 바이저가 다시 Cent OS의 Kernel로 명령어를 전달한다.

사실 이때만 봐도 커널과, OS가 각각 2개이다 (많은 리소스를 소모한다)

이때 하이퍼 바이저는 논리적으로 CPU와 Memory 자원을 분리하는 역할을 한다.

 

2. 반면 도커엔진은 Guest OS인 Ubuntu와 Ubuntu의 Kernel 없이 앱의 명령을 내 host OS인 Cent OS의 kernel에 전달 할 수 있다.

이는 도커엔진이 Ubuntu(guest OS) 위에서 실행되는 app의 명령어를 Cent OS(host OS) kernel이 이해할 수 있게 바꿔주는 역할을 하기 때문이다.

하이퍼 바이저와 비교해서 리소스 소모가 훨씬 적다.

 

하이퍼 바이저와는 다르게 도커는 커널을 설치하지 않아, 베이스 이미지에도 커널이 없다. 따라서 하이퍼바이저에서 서버를 설치할 때 쓰는 윈도우나 리눅스 이미지 파일(iso)에 비해서 도커 이미지는 (베이스 이미지 조차도) 훨씬 가볍다

가벼운만큼 설치가 엄청 빠르다!!! (컨테이너 환경은 가볍다)

 

LXC(Linux Containers)

컨테이너 환경이 실제 메모리 상에 어떤식으로 저장될지를 생각해보면,

리눅스에 프로세스의 네임스페이스를 다르게 해서 분리해둔 것이라고 생각하면된다. 리눅스에 격리 구역을 만들어서 서로 엉키지 않게 분리해놓고 그냥 프로세스를 실행하는 것이다

이 작업들을 LXC가 해준다

 

LCX는 리눅스 커널 컨테이너 기능을 위한 사용자 영역 인터페이스다. 리눅스 사용자가 쉽게 시스템과 어플리케이션의 컨테이너들을 생성/관리 할 수 있게 해준다.

공식문서에 따르면 아래와 같은 기능이 있다 한다. (근데 읽어도 솔직히 잘 모르겠다)

  • 커널 네임스페이스 (ipc, uts, 마운트, pid, 네트워크, 사용자)
  • Seccomp 정책
  • Chroots (pivot_root 사용)
  • 커널 캐퍼빌리티
  • CGroups (컨트롤 그룹)
  • Apparmor와 SELinux 프로파일

그래서 그냥 컨테이너 환경에서는 도커 위에 올라간 코드를 돌린다는 것은

  1. LCX를 통해 리눅스 커널안에 Cgroup이라는 구분자와 NameSpace라는 구분자를 결합하여 격리된 구역을 만든다.
  2. 해당 격리 구역에 있는 코드를 도커엔진은 LXC를 사용하여 격리된 구역 안에서 프로그램을 실행한다
  3. 즉, 도커엔진이 리눅스에서 돌 때 LXC를 사용하여 위에 말한 host OS kernel 명령어로의 치환이 이루어 지고 구역을 격리시킨다는 것이다

첨언하자면 옛날의 윈도우의 도커는

하이퍼바이저위에 geust OS로 리눅스를 설치하고 그위에서 도커엔진을 돌렸다고한다 (리소스 절약이 없었을 것)

요즘엔 그냥 된다고 한다.(최근 윈도우에 리눅스 커널[WSL]이 들어왔다고..mac유저라 관심이 없었음)

 

[연결 글]

2020/07/12 - [Develop/DevOps] - 알아두면 언젠간 깨달을 도커지식 2 - 도커 네트워크

 

알아두면 언젠간 깨달을 도커지식 2 - 도커 네트워크

이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다 필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다! 그리고 항상

jyami.tistory.com

 

This post is written in a rough-and-ready style and will be updated as I learn more
This may reflect a highly subjective perspective from my experience studying Docker, so please leave a kind comment if anything seems off!
And shoutout to Youngwoo for always guiding me down the path of Docker!

 

Server Virtualization

There are two types of virtualization.

  1. Hypervisor Virtualization
  2. Container Virtualization

 

Hypervisor Virtualization

Hypervisor-based virtualization works by installing a host OS on your server, partitioning resources as needed to create virtual machines, then installing a Guest OS on each VM to run applications on top of it.

However, with hypervisor-based virtualization, each VM uses its own separately allocated resources — and when there are duplicate resources across VMs, it essentially wastes unnecessary capacity (resources).

 

Container Virtualization

So what came along to replace this OS-based virtualization technology is Container technology. This container technology is called LXC (Linux Containers), and Docker is something built by leveraging this LXC technology really well.

 

In a Docker environment, as shown in the diagram above, a Guest OS is not needed. Let's walk through an example for easier understanding.

 

The Host OS is CentOS, and you want to run an application on Ubuntu.

1. With a hypervisor, you install Ubuntu as the Guest OS and run the app on top of it. Then the kernel in the Guest OS passes commands to the hypervisor, and the hypervisor relays them to the CentOS kernel.

Just looking at this, there are already two kernels and two OSes (consuming a lot of resources).

Here, the hypervisor's role is to logically separate CPU and memory resources.

 

2. On the other hand, the Docker engine can deliver app commands directly to the host OS (CentOS) kernel without needing the Guest OS (Ubuntu) or Ubuntu's kernel.

This is because the Docker engine translates the commands from apps running on Ubuntu (Guest OS) into something the CentOS (host OS) kernel can understand.

Compared to a hypervisor, resource consumption is significantly lower.

 

Unlike a hypervisor, Docker doesn't install a kernel, so even the base image has no kernel. Therefore, Docker images (even base images) are much lighter compared to the Windows or Linux image files (ISO) used when setting up servers in a hypervisor.

Being lightweight means installation is incredibly fast!!! (Container environments are lightweight)

 

LXC (Linux Containers)

If you think about how a container environment is actually stored in memory,

you can think of it as processes separated by different namespaces in Linux. It creates isolated zones in Linux so things don't get tangled up, and simply runs processes within those zones.

This is what LXC does for us.

 

LXC is a userspace interface for the Linux kernel containment features. It lets Linux users easily create and manage system and application containers.

According to the official documentation, it has the following features. (Though honestly, I still don't fully get it even after reading it)

  • Kernel namespaces (ipc, uts, mount, pid, network, user)
  • Seccomp policies
  • Chroots (using pivot_root)
  • Kernel capabilities
  • CGroups (control groups)
  • Apparmor and SELinux profiles

So, running code on top of Docker in a container environment basically means:

  1. Through LXC, isolated zones are created inside the Linux kernel by combining identifiers called Cgroups and Namespaces.
  2. The Docker engine uses LXC to run programs inside those isolated zones.
  3. In other words, when the Docker engine runs on Linux, it uses LXC to translate commands into host OS kernel commands (as mentioned above) and isolate the zones.

As a side note, Docker on Windows in the old days

used to install Linux as a Guest OS on top of a hypervisor and run the Docker engine on that (so there were no resource savings).

Nowadays it just works natively, they say. (Apparently a Linux kernel [WSL] was recently added to Windows..I'm a Mac user so I didn't really care)

 

[Related Post]

2020/07/12 - [Develop/DevOps] - Docker Knowledge You'll Eventually Appreciate 2 - Docker Networking

 

Docker Knowledge You'll Eventually Appreciate 2 - Docker Networking

이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다 필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다! 그리고 항상

jyami.tistory.com

 

댓글

Comments