728x90

우리는 Spring Framework에 대해 많이 들어보았고, 공부했고, 사용을 해보았을 것입니다.

 

기본적으로 Bean 어노테이션을 사용하여 Bean으로 등록하여 개발자가 직접 생성을 하여 사용하지 않아도, Spring Context 내에서 Bean을 인식하고 생성하여 관리하도록 하였을 것입니다.

 

이번 글에서 어떻게 사용할 Bean으로 인지하게 할 수 있는지, 또 어떻게 사용할 수 있을지 등을 확인해 보도록 하겠습니다.

Bean 등록

Bean으로 등록하려면 여러 가지 방법이 존재합니다.

1. @Bean

Bean 어노테이션의 경우 개발자가 직접 제어가 불가능한 외부 라이브러리등을 Bean으로 만들려 할 때 사용합니다.

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
    @AliasFor("name")
    String[] value() default {};

    @AliasFor("value")
    String[] name() default {};

    boolean autowireCandidate() default true;

    String initMethod() default "";

    String destroyMethod() default "(inferred)";
}

Bean어노테이션 내부를 보면 위와 같이 구현되어 있으며 사용할 때는 아래와 같이 정의를 할 수 있습니다.

@Configuration
public class TestConfiguration {

    @Bean
    public String testDescription() {
        return "test";
    }
}

위와 같이 정의를 한다면 이제 testDescription이라는 Bean이 생성되는 것이고 이를 사용하려면 @Autowired와 같이 메서드 명으로 주입받아 사용할 수 있습니다.

@Autowired
private String testDescription;

 

2. @Component

개발자가 개발한 Class를 Bean으로 등록하기 위해 사용하는 어노테이션입니다.

 

@Bean어노테이션과 다르게 Class자체를 Bean으로 등록하는 것으로, 위의 @Bean어노테이션을 사용하여 Bean으로 등록해도 괜찮지만, 개발자가 제어가 가능하여 @Component를 사용합니다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}

어노테이션 자체는 위와 같이 구현되어 있으며, Class, interface, enum 등에 지정할 수 있습니다.

 

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    @Autowired
    private Test testDescription;

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

위 controller를 보면 Test라는 클래스를 Bean으로 등록하고, 이를 사용하는 부분인데 , Bean과 다르게 변수명을 클래스 명과 다르게 명시하였음에도 정상적으로 주입받는 것을 확인할 수 있습니다.

 

이를 통해 @Component어노테이션을 사용하여 Bean으로 등록할 경우 주입받는 Type에 맞춰 주입받는 것을 확인할 수 있습니다.

 

Bean 주입

위에 글에서도 나왔듯 @Autowired처럼 Bean들을 주입받을 수 있는 방법들이 존재며,

이는 생성자 주입(Constructor Injection), 필드 주입(Field Injection), 수정자 주입(Setter Injection)등이 존재합니다.

1. Constructor

해당 객체가 Bean으로 등록될 때 생성자에 명시되어 있는 파라미터들을 즉 Bean들을 주입받는 방식입니다.

 

가장 기본적이라 생각하며, 가장 코드 줄이 긴 방법이지만 가장 직관적인 만큼 오류가 가장 발생하지 않는 방법입니다.

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    private final Test testDescription;

    public ConnectTestController(Test testDescription) {
    	//주입받는 부분
        this.testDescription = testDescription;
    }

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

2. @RequiredArgsConstructor

lombok의 @RequiredArgsConstructor를 사용하는 방법입니다.

@RestController
@RequestMapping("/connect")
@RequiredArgsConstructor
public class ConnectTestController {
    //
    private final Test testDescription;

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

위와 같이 생성자를 직접 명시하지 않고 사용하는 방법이며 final로 지정된 전역변수들에 한해서 빈을 자동으로 주입해 줍니다.

3. @Autowired

위 방법들과 다르게 필드 주입 혹은 수정자 주입받을 때 사용하는 방법입니다.

기본적으로 Springframework에서 제공해 주는 @Autowired어노테이션을 사용합니다.

필드 주입

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    @Autowired
    private Test testDescription;

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

수정자 주입

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    private Test testDescription;

    @Autowired(required = false)
    public void setTestDescription(Test testDescription) {
        this.testDescription = testDescription;
    }

    @GetMapping()
    public String connect() {
        System.out.println(testDescription.testDescription());
        return "OK";
    }
}

위처럼 두 가지 방법으로 구현이 가능한 방법입니다.

 

생성자 주입방식보다 번거롭고 코드 줄이 길어 가독성이 불편하며, 개발자 실수가 많이 이뤄질 수 있는 방법으로 권장하는 Bean주입 방법은 생성자 주입방법을 권장하고 있습니다.

 

 

기타

@Qualifier

Qualifier은 동일한 타입의 Bean들이 많을 때 어떤 Bean을 주입받을지 명시할 때 사용하는 어노테이션입니다.

@Component
public class Test {

    public String text = "test";

    @Bean
    @Qualifier("test1")
    public Test testDescription() {
        Test test =  new Test();
        test.text = "hello";
        return test;
    }
}

우선 Bean을 2개 지정해 보도록 하겠습니다. 하나는 Test라는 Bean이고 하나는 test1이라는 Bean을 등록하였습니다.

위처럼 @Qualifier를 지정하여 사용할 경우 해당 이름으로  Bean이 생성됩니다.

 

@RestController
@RequestMapping("/connect")
public class ConnectTestController {
    //
    @Autowired
    @Qualifier("test1")
    private Test bean;
    @Autowired
    private Test test;

    @GetMapping()
    public String connect() {
        System.out.println(bean.text);
        System.out.println(test.text);
        return "OK";
    }
}

 이후 위처럼 각각 정의를 하고 실행을 하였을 때 아래와 같은 실험 결과를 얻어볼 수 있습니다.

 

이처럼 같은 타입을 반환하는 Bean들 중 하나를 지정하여 주입받을 때 사용하는 어노테이션입니다.

728x90

'Server' 카테고리의 다른 글

[스터디] JPA에 대하여  (0) 2024.05.25
[lombok]RequiredArgsConstructord와 Qualifier  (0) 2024.05.03
SSE란? - Server Sent Events  (2) 2024.04.18
REST API란?  (0) 2024.04.14
[Docker] 도커로 데이터 베이스 편하게 사용하자~  (1) 2024.04.08
728x90

SSE(Server Sent Events)란 서버 주체 단방향 통신 기술입니다.

이것이 무슨 말이냐 우리가 기본적으로 사용하는 일반적인 HTTP 통신은 Client가 요청을 보내면 서버는 해당 요청 건에 대해서만 응답을 할 수 있는 규칙을 가지고 있습니다.

 

이럴 경우 우리가 개발한 서버에서 어떤 작업이 이뤄졌을 때 Client가 알 수 있는 방법은 Client의 요청이 있을 때까지 기다리는 방법 밖에는 없습니다. 하지만 이렇게 구현을 하자면 Client는 일정 시간이 지날 때마다 서버를 조회하여 데이터를 최신화 시켜주는 방법 밖에 없을 것이고 이는 무분별한 호출로 인한 리소스 낭비로 인해 문제를 야기할 수도 있습니다.

 

그럼 서버에서 응답을 보내서 Client에서 처리하면 어떨까? 해서 나온 것이 웹소켓과 SSE기술입니다.

웹소켓의 경우 양방향 통신으로 handshaking방식으로 이루어집니다. 이는 Client와 서버의 통신이 연결되는 방식을 말하며 웹소켓에서 한번 연결된 통신은 지속적으로 유지되며, 이 통신을 통해 Client와 서버는 데이터를 주고 받을 수 있습니다. 이는 채팅과 같이 실시간 소통이 이뤄져야하는 기능을 구현 할 때 많이 사용되는 방법입니다.

 

하지만 알람과 같이 Client가 서버로 요청을 보내지 않아도 서버에서 특정 작업이 생겼을 때 Client가 알게 하고 싶다면 WebSocket방식은 과한 기능 구현인 것 같을 수 있습니다. 그럴때는 SSE를 적용하는 것을 고민해볼 수 있습니다.

 

SSE는 Client의 요청을 기다리지 않고 서버에서 Client로 요청을 보내는 기술로 서버에서 단방향 통신으로 Client로 응답을 보낼 수 있습니다. Client가 처음 한번의 요청을 통해 Client와 서버의 연결이 설정된 후 서버의 일방적인 데이터 전송이 가능해 집니다.

 

사용방법(Client - React)

Client의 설정은 간단합니다. 위에서 말했듯 Client에서의 작업은 초기 연결 설정을 위한 요청과 Event를 수신하여 작업할 부분만 작업해주면 됩니다.

 

useEffect(() => {
    let eventSource: EventSource;
    if (requestId.length) {
    	// 서버와 연결
        eventSource = new EventSource(defaultURL + "/connect/" + requestId);
        // 이벤트 수신 시 수행할 작업들
        eventSource.addEventListener("message", handleEventMessage);
        eventSource.addEventListener("error", handleEventError);
    }

    return (() => {
        if (requestId) {
        	// 페이지가 종료될 때 설정한 이벤트들도 제거
            eventSource.removeEventListener("message", handleEventMessage);
            eventSource.removeEventListener("error", handleEventError);
        }
    })
}, [requestId]);

위 설정은 서버와의 초기 연결을 요청 보내는 부분입니다.

new EventSource()는 초기 서버 연결을 담당해주는 부분이며, 내부 파라미터로 url을 주입해주면 해당 URL로 Get요청을 보내줍니다.

 

이때  요청 Header의 Content-Typetext/event-stream으로 지정되어 있습니다.

 

사용방법(Back-Spring)

서버는 Client보다 작업량 자체는 많지만 다른 웹소켓과 같은 기술들에 비하면 훨씬 적은 설정으로 구현할 수 있습니다.

 

우선 서버는 Client로부터 연결을 위한 요청이 들어온다면 HTTP Method와 produces즉 미디어 타입을 명시 해주어야합니다.

@GetMapping(value = "/connect/{requestId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@PathVariable String requestId) {
    // SseEmitter설정을 위해 sseService의 register을 사용
    return sseService.register(requestId);
}
더보기

SseEmitter란?

SseEmitter란 Spring framework에서 sse기술을 적용해 관리하기 위하여 지원해주는 객체로써 해당 객체를 사용함으로써 개발자는 손쉽게 Sse기술을 적용할 수 있습니다.

 

해당 객체가 상속받은 ResponseBodyEmitter를 보면 하나 이상의 객체가 응답에 기록되는 비동기 요청을 처리하기 위한 컨트롤러 반환 타입이라고 명시가 되어있습니다.

 

이런 객체를 상속받아 구현된 객체인 만큼 비동기를 지원하며,  Spring framework 4.2부터 사용가능합니다.

@Service
public class SseService {
    //
    private final long TIMEOUT_MILLISEC = 60L * 60 * 1000;
    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
    
    public SseEmitter register(String requestId) {
        SseEmitter emitter = new SseEmitter(TIMEOUT_MILLISEC);
        config(emitter, requestId);
        emitterMap.put(requestId, emitter);
        return emitter;
    }    

    private void config(SseEmitter emitter, String requestId) {
        emitter.onCompletion(() -> emitterMap.remove(requestId));
        emitter.onTimeout(() -> emitterMap.remove(requestId));
    }
}

sseService의 register구현 부분입니다.

생성시 타임 아웃을 지정할 수 있으며, 해당 값은 연결된 이후 얼마나 시간을 유지 할 것인 지를 명시합니다.

해당 시간이 지나 요청을 할경우 onTimeout() 메소드가 자동합니다.

 

ConcurrentHashMap를 사용하여 inMemory로 관리를 하였으며, Thread safe인 자료구조를 사용하여 통신 연결이 MultiThread환경에서 공유되도록 구현하였습니다.

더보기

※ 주의

여러개의 Was환경을 구축한 경우 Redis를 활용하여 동기화되도록 구현 해야합니다.

위 설정을 통해 Client와 서버의 통신은 지속적으로 연결이 되며, 서버에서 client로 계속 데이터를 보낼 수 있는 환경이 구축 된 것 입니다.

 

Event send

서버에서 이벤트를 보내려면 기존 emitterMap에 저장해 놓은 sseEmitter를 사용하여 메시지를 보내게 됩니다.

public <E> void send(SseEmitter emitter, E data) {
    try {
        emitter.send(
                SseEmitter
                        .event()
                        .name("message")
                        .data(data)
        );
    } catch (IOException e) {
        emitter.completeWithError(e);
        log.error(e.getMessage(), e);
    }
}

emitter를 사용하여 send를 하게 되면 연결설정 되어있는 브라우저로 이벤트가 날라가게 되고, 

위와 같이 지속적으로 웹브라우저 EventStream에 이벤트가 쌓이게 됩니다.

 

SseEmitter.event()에는 이벤트에 대해 설정을 할 수 있습니다.

 - .id(): Event의 ID를 설정할 수 있습니다.

 - .name(): Event의 Name을 설정할 수 있습니다. 이 이름은 Client에서 EventListener가 event를 식별하여 가져가는 기준이 됩니다.

 - .data(): Event에 담겨있는 데이터입니다.

 

해당 통신을 종료하려면 complete()를 사용해야 하며, 통신 종료를 해주지 않으면 timeOut될 때까지 통신이 유지되어 메모리 누수 현상이 발생할 수도 있습니다.

사용시 주의

Sse는 Http1의 버전에서는 브라우저 6개에 한해서 지원해주는 기능입니다.

이는 브라우저 6개가 통신 연결이 되어있다면, 더이상의 연결이 불가한 사항인데, 이러한 점을 브라우저에서는 지원을 해주지 않고, 개선도 안되는 것으로 알고 있습니다.

 

하지만 Http2버전 부터는 100개까지 지원이 가능하다 하니 사용하시는 Http버전에 잘 맞춰 사용하시기 바랍니다.

 

2024.04.19 update

Font에서 EventSource 객체를 사용할 때 주의점

EventSource객체를 통해 통신 연결을 시도하게 되면 서버에서 complete를 하더라도 계속해서 연결을 시도하려합니다.

즉 작업이 끝나 통신을 서버에서 끊었음에도 API를 계속 호출하는 상황이 발생합니다.

 

이를 방지하기 위해 서버와 Client는 종료되는 부분을 명확하게 정하여 EventSource를 close해주시기 바랍니다.

728x90
728x90

서버 개발을 하면서 REST API를 많이 접해볼 수 있습니다.
오늘은 이 REST가 무엇이며 일반 API와 REST API는 어떤 차이점을 가지고 있는지 알아보도록 하겠습니다.

REST란?

우선 REST API에 대해 알려면 REST가 무엇인지부터 알아야 합니다.

REST의 FullName은 REpresentational State Transfer인데, 직역하자면 '대표 상태 전송'이라고 해석할 수 있습니다.

REST는 HTTP프로토콜을 사용하며 분산형 하이퍼미디어 시스템의 아키텍처 스타일입니다. 

더보기

분산 하이퍼미디어 시스템(distributed hypermedia systems)이란?

여러 대의 컴퓨터나 네트워크로 구성된 시스템에서 하이퍼 미디어를 관리하고 제공하는 시스템

 

하이퍼미디어는 텍스트, 그래픽, 오디오, 비디오 등 다양한 멀티미디어 자료를 연결하여 구성한 것을 의미합니다.

즉 시스템 간 정보를 주고받는 시스템을 구축할 때 적용하는 아키텍처 스타일이라고 쉽게 표현할 수 있습니다.

이는 프로토콜이나 표준이 아니고 네트워크를 통해 애플리케이션을 느슨하게 설계하기 위한 지침으로 굳이 안 지켜도 됩니다.

다만 REST스타일을 벗어나게 된다면 REST API가 일반 API가 될 뿐입니다.

 

REST 구성

REST는  HTTP를 사용한 아키텍처 스타일이다 보니 HTTP에서 제공하는 자원들을 활용합니다.

1. 자원 - HTTP URL

REST에서 URL은 어디로 갈 것인지 혹은 어떤 정보를 가지고 있는지 즉 정보의 자원을 명시합니다.

즉 URL에는 행위에 관련된 표현이 들어가는 것은 부적절하며, 명사가 들어가는 것이 좋습니다.

2. 행위 - HTTP Method

REST는 HTTP Method로 어떤 행위를 할 것인지를 표현합니다.

행위는 보통 5가지를 사용하며, 이 행위에 대한 것은 아래 표로 정리해 보았습니다.

Method Action
GET 리소스를 조회
POST 리소스 생성
PUT 리소스 수정
PATCH 리소스 부분 수정
DELETE 리소스 삭제

3. 표현 - HTTP Message Pay Load

통신에서 반환된 리소스를 반환하며 리소스는 JSON or XML or HTML 등의 타입을 가집니다.

이때 통신 상태에 대한 HTTP Status가 반환되는데 코드에 대한 의미는 다음과 같습니다.

Code Description
1XX 서버가 요청을 받았으니 클라이언트는 작업을 계속 진행할 것을 의미
2XX 요청을 완료하였음을 의미
3XX URL이 변경되었거나 다른 URL을 호출 할경우
4XX Client에서 서버를 호출할 때 잘못된 호출을 할 경우
5XX 서버에서 요청을 처리 혹은 서버 자체가 잘못 되었을 경우

REST의 제약 조건

REST를 적용하려면 몇 가지 조건이 지켜져야 RESTful하다 할 수 있는데 어떤 조건들이 있는지 알아보겠습니다.

1. Client-Server

클라이언트와 서버의 완전 분리입니다.

클라이언트와 서버를 분리하여 개발을 함으로써 서로 변경사항에 대해 영향을 주지 않기 위함입니다.

 

클라이언트는 URI정보를 통해 서버를 호출할 수 있어야 하며,

서버는 HTTP를 통해 들어온 요청에 대해 데이터를 전달할 수 있습니다.

2. Stateless

Stateless 즉 무상태는 서버에서 클라이언트의 상태에 대한 정보를 가지고 있지 않다는 의미입니다.

이는 Stateless의 반대인 Stateful을 보면 더욱 쉽게 이해할 수 있습니다.

 

Stateful은 서버가 클라이언트의 상태를 가지고 있다는 것을 의미하며, 이는 클라이언트와 서버의 통신은 연결되어 있어 작업을 할 때 서버에서 클라이언트 상태를 보고 작업을 하는 것입니다. 이럴 경우 통신 중이던 서버에 장애가 생길 경우  클라이언트는 그동안 서버에 저장된 상태정보를 유실하게 되어 처음부터 통신을 시작해야 하는 경우가 생깁니다.

 

Stateless는 서버가 클라이언트의 상태를 가지고 있지 않아 클라이언트가 서버를 호출할 때마다 필요한 상태 정보들을 같이 넘겨줘야 한다는 번거로움이 존재합니다. 하지만 위의 경우처럼 서버가 장애가 생겼을 때 같은 기능을 하는 다른 서버가 해당 작업을 이어받아 처리할 수 있게 할 수 있습니다. 즉 서버 확장에 대해 Stateful에 비해 자유롭다고 볼 수 있습니다.

3. Cache

서버에서 응답을 줄 때 해당 리소스의 캐싱 가능 여부와 응답 캐싱기간을 알려주는 정보가 포함되어있어야 하며, 캐싱할 수 있어야 합니다.

이는 클라이언트와 서버의 성능을 향상하는데 도움을 줍니다.

4. Uniform Interface

Uniform Interface는 REST API인가?를 결정하는 가장 중요한데, 이는 클라이언트와 서버의 통신이 일관되고 표준화된 방식이 되도록 유도하는 요소입니다.

 

Uniform Interface는 4가지로 구성되어 있습니다.

- Identification of resources

각 리소스들은 고유한 URI를 가져 식별되어야 합니다.

쉽게 말하면 API를 생성할 때 URI는 고유하게 식별되어 위치를 특정할 수 있어야 한다는 의미입니다.

 

- Manipulation of resources though represenations

이것을 이해하기 위해선 우리는 우선 Represenation에 대해 알아야합니다.

GET http://localhost:8080/helth
Accept:text/plain
Accept-Language:en-US;q=0.8,en;q=0.7

위와 같이 서버가 잘 작동하고 있는지 확인하는 API를 호출한다고 가정해보겠습니다.

그럼 우리가 전달받는 리소스는 "OK"라는 데이터를 받을 수 있을 것 입니다.

하지만 이는 잘못된 설명이였습니다.

 

그 이유는 "OK"라는 것은 리소스가 아닌 represenation data이기 때문입니다.

왜 리소스가 아니냐면 우리는 "OK"라는 데이터를 응답으로 받았지만 여기서 리소스는 "확인을 위한 의미를 담은 문서"이기 때문입니다.

즉 represenation data는 'text/plain' 타입의 영어로 된 데이터가 되는 것 입니다.

 

그리하여 클라이언트는 서버에 'text/palin'을 표현해주길 원하면 텍스트 데이터를, 'text/html'을 표현해주길 원하면 html로 된 데이터를 전달 받을 수 있는 것 입니다. ( 물론 여러 표현방법에 대한 API는 서버에 구현이 되어있어야 합니다 안할경우 406error!! )

 

그럼 Manipulation of resources though represenations는 무슨 의미인가?

요청하는 URI는 동일하지만 표현방법에 따라 응답할때 전달하는 represenation Data 가 둘이상 지원된다는 의미입니다.

즉 URI가 같더라도 원하는 Represenation에 따라 여러 데이터를 응답 받을 수 있다로 설명이 가능할 것 같습니다. 

 

- Self-descrive messages

각 응답과 요청은 스스로 설명이 가능해야 합니다.

이게 무슨 의미냐면, 응답받은 데이터들이 JSON으로 왔다고 가정해 보겠습니다.

{
    "name": "hong gil dong",
    "description": "This is a living thing."
}

위와 같은 리소스를 응답으로 받았다면, 우리는 저 name이 사람이름인지 동물이름인지 알 수 있는 방법이 없을 것입니다.

이럴 때 응답만을 보고 저게 사람이름인지 동물이름인지 알 수 있게 자체적으로 설명이 있어야 한다는 뜻입니다.

 

Spring REST Docs와 같은 프레임워크를 활용하여 응답 리소스에 Doc의 link를 실어 보내주는 방법으로 해결을 해야합니다.

 

- Hypermedia as the engine of application state ( HATEOAS )

클라이언트는 서버로부터 전송받는 응답으로 다음 가능한 행위를 결정할 수 있어야합니다.

이게 무슨 뜻인가!

이전에 우리는 이름과 설명을 응답으로 받았습니다.

여기에 설정을 붙여보겠습니다.

우리가 만약 사람들 관리 사이트에서 유저 상세 화면을 개발하고 있다고 가정을 해보겠습니다.

이때 우리는 기본적으로 해당 유저에 대한 정보들을 가져오게 될 것 입니다.

{
    "name": "hong gil dong",
    "age": 27,
    "phoneNumber": "010-0000-0000"
}

이제 우리는 저 정보들을 가지고 개발을 하게 될 것입니다.

하지만 우리가 페이지 내에서 다음 사람을 보고 싶다거나 할 때 어떤 행위를 해야할 지 위 응답에는 포함되어있지 않습니다.

 

{
    "name": "hong gil dong",
    "age": 27,
    "phoneNumber": "010-0000-0000",
    "links": [
    	{
        	"rel": "self",
            	"href": "http://localhost:8080/user/2"
        },
    	{
        	"rel": "befor",
            	"href": "http://localhost:8080/user/1"
        },
    	{
        	"rel": "after",
            	"href": "http://localhost:8080/user/3"
        }
    ]
}

위와 같이 Hateoas를 적용한다면 이전 유저를 조회하려면 어떤 행위를 해야하는지, 이후 유저를 조회하려면 어떻게 해야하는지에 대한 정보들이 응답에 포함되어있습니다.

 

이로인해 클라이언트는 다른 정보 없이 응답받은 정보들 속에서 다음 행위를 결정할 수 있게 되는 것 입니다.

5. Layered System

클라이언트나 서버 사이 중간계층들을 둘 수 있도록 확장성을 열어 놓은 것을 의미합니다.

미들웨어, 프록시, 게이트웨이, 캐시 서버, 로드 밸런서 등이 이에 포함되며, 이렇게 중간계층이 추가된다 하더라도 클라이언트와 서버의 상호 작용에는 영향이 없어야합니다.

 

6. Code-On-Demand ( Optional )

REST 조건이긴하지만 필수 제약 조건은 아닙니다.

클라이언트가 서버에 코드를 요청시 서버는 클라이언트에서 실행될 소프트웨어 코드를 보내는 것이라 할 수 있습니다.

Java applets와 javaScript등이 있습니다. 

REST API

위와 같이 REST의 조건들을 지켜 API를 개발하였을 경우 이를 REST API라고 하며 이러한 REST API들로 개발된 웹 서비스를 'RESTful 하다' 라고 표현할 수 있습니다.

 

여러가지 장단점이 있는데, 

 

REST가 표준이 존재하지 않고 아키텍처 스타일이다보니 발생하는 장단점이 크다고 생각합니다.

장점은 위에서 계속 나왔듯이 확장성과 자율성이 뛰어난 API를 구축할 수 있다로 볼 수 있고,

단점은 너무 자율성이 높고 조건들이 많다보니 지켜지기 쉽지 않다고 볼 수 있습니다.

 

마치며

개인적으로 1, 2, 3, 5번은 HTTP API를 개발하면서 크게 신경쓰지 않아도 지켜지는 조건들이라고 생각합니다.

다만 4번의 조건들이 신경쓸 부분들이 많은 만큼 만약 REST API를 만들고 싶다면 조건들을 세심히 따져보시기 바랍니다.

 

REST API는 표준이 정해져 있지 않지만 조건들이 존재하는 만큼 어느정도 커스텀은 괜찮다고 보지만, 조건이 맞지 않다면 그냥 API개발했구나 생각하시면 될 것 같습니다. ( 제가 그랬습니다 ㅎㅎ )  


 참조

https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm

https://blog.npcode.com/2017/04/03/rest%EC%9D%98-representation%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80/

728x90
728x90

목차

  1. 이유
  2. 사용방법
  3. 후기

이유

local에서 개발하고 테스트 하다보면
여러개의 데이터 베이스를 생성 혹은 처음 프로젝트를 시작할 때 좀 깔끔한 상태로 시작하고 싶다는 생각을 할 수 있다.

개인적으로 이런 생각을 가지게 된 데에는 포트 충돌이 가장 큰 원인이였다.

예를 들어 Mysql을 사용하다 MariaDB를 사용하게 되어 설치를 하려하는데 포트가 충돌 되는 현상이 발생하였다.

무슨일일까?

Mysql을 설치할 당시 기본 포트로 3306번을 할당해 주게 되는데 MariaDB또한 3306포트를 기본으로 할당해주어 포트 충돌이 생긴 것이다.

물론 새로 설치하는 MariaDB를 3307로 바꿔 설치하면 되지만,
필자 처럼 특정 이유 없이 여러 포트에 생기는 것을 별로 안좋아하는 사람들도 있을 수 있다.

그래서 그때 부터 고민을 하게 되었는데

어차피 여러 서비스를 image로 만들어 container에 띄우게 되면 container만 갈아 치우는 느낌으로 하나의 포트만 두고 사용할 때만 꺼내 쓸 수 있는 데이터 베이스가 되지 않을 까 였다.

해서 이번 글에서는 위의 해결 방법에 대해 다루려 한다.

사용방법

우선 Docker를 설치해야하는데 이는 다음 글을 참고 하기 바란다.

Docker 설치하는 방법

Docker를 설치하였으면

이제 본인이 사용할 Image를 다운 받으면 된다.

1. 본인이 사용할 image를 검색한다.

우리가 데이터 베이스 이미지를 사용하기 위해선 docker hub에서 지원해주는 image를 다운받아 사용해야하는데 그 첫번째로 사용할 image를 검색하는 것이다.

docker search ${검색할 이미지 명}
ex) docker search mysql

#### 2. 검색한 image를 다운받는다.
>```bash
docker pull ${다운받을 이미지 명}
ex) docker pull mysql

3. image를 container로 실행시킨다.

--name: 실행시킬 container의 이름을 지정
-- MYSQL_ROOT_PASSWORD : container를 실행할 때 mysql의 root계정의 비밀번호를 지정
-- 3306:3306 mysql을 port 3306으로 설정 하여 실행

docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=<password> -d -p 3306:3306 mysql:latest

#### 4. Docker container 시작/중지/정지
>```bash
# Docker 컨테이너 중지
$ docker stop ${container명}
# Docker 컨테이너 시작
$ docker start ${container명}
# Docker 컨테이너 재시작
$ docker restart ${container명}

4. 실행중인 Docker container 접속

docker exec -it ${container명} bash

#### 5. Mysql 접속
>```bash
  mysql -u ${mysql계정아이디} -p ${mysql비밀번호}

위 과정을 거치면 정상적으로 사용할 수 있다.

후기

업무상 본인 노트북을 가지고 다니며 업무용, 공부용, 프로젝트용으로 한번에 사용하다 보니 여러 데이터 베이스를 사용하는 경우가 많이 생기게 된다.

이때 헷깔리지 않고 잘 관리를 하기 위해 고민하였고 사용하고 있는데 확실히 편하다.

개인적으로 여러개를 동시에 실행시키면 노트북이 버벅 거릴 수도 있는데 솔찍히 말하면 컴퓨터 성능이 좋아 아직 그정도의 불편함은 느끼지 못하였고,
솔찍히 편하고 좋다.

한줄평

굉장히 편한거 같고 사용하는 데이터 베이스에 맞춰 컨테이너만 갈아 끼워주면 되서 좋다!!

728x90

'Server' 카테고리의 다른 글

SSE란? - Server Sent Events  (2) 2024.04.18
REST API란?  (0) 2024.04.14
[Docker] Docker 설치하기  (0) 2024.04.08
[Docker] Docker란 무엇인가?  (2) 2024.04.08
NATS란?  (0) 2024.04.08
728x90


이번 글에서는 Docker시리즈의 기본 Docker를 설치하는 방법에 대해 설명해보려한다.

Docker가 무엇인지에 대해 알고 싶으면 아래 링크를 참고하기 바란다.

Docker란 무엇인가?

사실 Docker공식 사이트에서 확인하면 되지만 그래도 필요한 분이 계실 거 같아 복습할겸 글을 써본다.

우선 설치 환경은 이렇다

OS: Ubuntu 22.04

따로 Kubernetes와 같은 것들은 따로 설치한다는걸 제외한 가정이기 때문에 가볍게 시작하겠다.

  1. apt-get을 최신으로 업데이트를 해준다.
  2. apt-get update
  3. apt-get install ca-certificates curl gnupg
  4. install -m 0755 -d /etc/apt/keyrings
  5. curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
  6. sudo chmod a+r /etc/apt/keyrings/docker.gpg
  7. echo \
    "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  8. 위 설정을 토대로 Docker을 받기 위해 apt-get 을 업데이트 시켜준다.
  9. sudo apt-get update
  10. docker 관련 패키지를 다운받는다.
  11. sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  12. docker 실행 docker run 만 작성해도 정상적으로 작동한다.
  13. sudo docker run hello-world
  14. docker가 정상적으로 작동 중인지 확인한다.
  15. systemctl status docker

참고

docker 설치 - https://docs.docker.com/desktop/install/ubuntu/

728x90
728x90


서버 개발자라면 한 번쯤 들어 보았을 Docker 과연 Docker가 무엇인가에 대한 호기심으로 인해 글을 작성한다.

Docker란?

Docker는 애플리케이션을 개발, 제공 및 실행하기 위한 개방형 플랫폼이라고 할 수 있다.

이게 뭘까??

단순하게 애플리케이션을 개발, 제공 및 실행하기 위해서는 그냥 실행하면 되지 않을까란 생각을 할 수도 있다.

이를 이해하기 위해서는 앞서 Docker의 Container가 왜 나왔는지 역사를 알아볼 필요가 있다.

Container 탄생기

서비스를 실행하기 위해서는 하나의 서버 할당은 당연한 수순이라 바라볼 수 있다.

이때 가정을 하나 해보도록 하자.

서비스를 배포하기 위해 하나의 서버를 할당하였는데 서비스를 배포하고도 서버의 성능이 많이 남는다고 가정하도록 하겠다.

이때 해당 서버에 또 다른 서비스를 배포하고 싶은데 이때 같은 서버를 사용할 경우 기존 서비스의 설정과 추가할 서비스의 설정이 충돌이 날 경우
개발자는 두 서비스 중 어떤 서비스를 서버에 배포할 것인지 고민할 수밖에 없다.

이렇듯 하나의 서버에 여러 서비스를 할당하기 위해 탄생한 것이 "서버 가상화"다.

서버 가상화는 물리적 서버 호스트에서 여러 개의 서버 운영 체제를 게스트로 실행할 수 있는 소프트웨어 아키텍처다.


위와 같이 하나의 서버에 n 개의 VM을 설치하여 각각 OS를 할당하여 서비스 간의 연결을 끊어버리는 방법으로 나왔다.

하지만 위의 방식을 보면 뭔가 무겁다는 생각이 들지 않나?

그렇다 서비스를 Host OS에 영향이 가지 않게 나눠 Guest OS로 서비스를 실행하였지만 각각의 VM OS를 구동하기 위한 파일들을 가지게 됨으로써 Host 서버의 메모리를 훨씬 잡아먹는 것이다.

그러면서 우리는 고민을 하게 될 텐데
서버 메모리가 부족해지면 어떻게 해야 할까??

서버를 더 좋은 서버를 구매하여 메모리를 늘릴까??
하지만 위 방법을 사용할 경우 만약 서비스를 구동하는 메모리를 초과한다면 또 메모리 낭비로 이뤄지게 된다.

그러면 OS를 따로 띄우는 것이 아니라 하나의 OS 위에 배포되는 서비스의 환경을 분리하면 되지 않을까?

이렇게 탄생한게 "Container"다.

Container는 위와 같이 Guest OS를 가지고 있지 않은 것을 확인 할 수 있는데,
이는 Linux 자체 기능인 chroot, namespace, cgroup을 사용하여 프로세스 단위의 격리환경을 구축하기 때문이다.

Container에는 Application을 구동하는데 필요한 라이브러리 및 실행파일만 존재하여 이미지로 만들었을 때 가상머신에 비해 이미지 용량이 대폭 주는 효과를 누릴 수 있다.

그럼 Container는 무엇인가??

Container란?

앞서 Container에는 Application을 구동하는데 필요한 라이브러리 및 실행파일만 존재한다고 하였다.

그리고 Container는 프로세스 단위의 격리환경이다.

이처럼 Container는 격리된 환경을 제공하며 프로세스의 생명주기를 관리하고있다.

즉 독립된 프로세스 개발환경을 보장받을 수 있는 환경 그것에 바로 Container라고 볼 수 있다.


그럼 Container는 Docker인가??

답은 아니요 다.

Container는 Docker에서 관리하는 프로세스의 단위 일 뿐 Docker가 Container라고 하기엔 너무 많은 기능이 Docker에 포함되어있다.

이제부터는 그 기능들에 대해 알아보겠다.

Docker의 기능은 무엇이 있을까?

크게 4가지로 볼 수 있는데

  1. 컨테이너 관리
  2. 이미지 관리
  3. 볼륨 관리
  4. 네트워크 관리

등이 있다.

하지만 위는 엄청 간략하게 말한것이고 어떤 기능들을 제공해 주는지 확인해보자!

이 사진은 Docker에서 어떤 기능을 지원해주는 지 확인할 수 있다.

Dockerd

dockerd는 container를 지속적으로 관리하는 백그라운드 프로세스(docker daemon)이다.

Docker API - Docker Client의 명령어를 /var/run/docker.sock unix소켓을 통해 API를 호출하여 dockerd가 작업을 수행한 이후 Client에게 Response를 return.

Docker CLI - Docker Command Line Interface

Storage mgmt - Container가 사용하는 파일 및 이미지 데이터를 효과적으로 관리하며, 컨테이너의 생성, 실행, 중지 및 제거등과 관련된 모든 작업에 관여한다.

libnetwork - 컨테이너의 네트워크 설정과 관련된 작업을 처리.

SwarmKit - Docker의 오케스트레이션 도구 즉 여러대의 호스트에서 컨테이너를 관리하고 조절하는기능을 제공.

  1. 서비스 디스커버리와 로드 밸런싱
  2. 서비스 복제 및 스케일링
  3. 다양한 네트워킹 옵션
  4. 상태 관리와 롤링 업데이트
  5. 보안 및 권한 관리

BuildKit - Docker 이미지를 빌드할때 사용하며 기존 docker build보다 더 빠르고 더 많은 기능과 유연성을 제공한다.

Docker Content Trust - Docker이미지의 보안을 강화하는 기능을 제공한다.

Image mgmt - Storage mgmt가 Container를 관리하면 Iamge mgmt는 Image를 관리한다.
기본적으로 Image다운로드 및 업로드, 빌드, 생성, 버전관리, 검색, 삭제 등의 기능들을 제공한다.


마무리

이로써 기본적인 Docker에 관해 알아보았다.

결국 Docker이미지 처럼 여러 container들을 관리하는 고래가 Docker라고 보면 될거 같다.

비록 하나하나 깊게 다루지 못했지만 기본적인 기능으로만 보았을때 생각보다 제공해주는 기능이 많아 좀더 열심히 공부해야 할거 같다.

한줄평

기본 설정만 잘해두면 서비스들을 배포하고 관리하는데 편하다!!


참고
https://docs.docker.com/get-started/overview/
https://www.youtube.com/watch?v=IiNI6XAYtrs

728x90
728x90

NATS란?

NATS는 클라우드 기반 애플리케이션, IoT 메시징 및 마이크로서비스 아키텍처를 위해 만들어진 가벼운 메시징 서비스입니다.

NATS Server는 Go언어로 구축되어있으나 NATS에서 제공하는 툴이나 라이브러리들을 많이 제공하고 있어 Cloud native Application, IoT Messaging, MSA등에서 많이 활용할 수 있습니다. 확인하러가기

NATS는 왜 사용하는 것인가??

  • 빠르다
  • 가볍다
  • 간편하다
  • 어디에든 배포가 가능하다
  • 최신 기술들 호환이 좋다

와 같은 여러가지 강점들을 가지고 있으며, 이를 토대로 kafka와 같은 Messaging 서비스들과 경쟁을 하고 있는 것 같습니다.

우선 NATS가 어떤 개념으로 작동하고, 어떤 기능들이 존재하는지에 대해 알아보도록 하겠습니다.


Subject-Based Messaging

NATS는 주제 기반 메시징을 지원하고 있습니다.
여기서 말하는 *주제는 무엇일까요?

Subject는 publisher와 subscriber가 서로를 알기 위한 문자열입니다.

이 Subject에는 몇가지 규칙이 따릅니다.

  • 권장 문자: a to b, A to Z 및 0 to 9(이름은 대소문자를 구분하며 공백을 포함할 수 없다.)
  • 특수문자: 마침표.(제목에서 토큰을 구분하는데 사용), *, > (각각이 와일드 카드로 사용됩니다)
  • 예약된 이름: $로 시작하는 이름은 시스템에 예약되어 있어 사용하면 안됩니다.

.은 이름의 그룹을 분간하기 위해 사용이 됩니다.

time.kr.soul
time.kr.busan

또한 *>는 이름이 매칭 부분을 조정할 수 있습니다.

*는 단일 토큰 매칭으로 *부분을 제외한 나머지 그룹군이 맞다면 매칭을 시켜줍니다.

>는 여러가지 토큰을 일치시켜줄 수 있는 것으로 이는 몇가지 조건을 맞춰야 사용할 수 있습니다.

  • 하나 이상의 토큰과 일치
  • 제목 끝에만 명시가능

위 조건을 만족할 경우 사용하여 토큰을 매칭시킬 수 있습니다.

와일드 카드는 혼합이 가능하며 *는 한 제목에 여러번 작성할 수 있습니다.
ex) *.*.east.>


Core NATS

NATS를 구성하는 모델들은 다음과 같습니다.

  • Publish-Subscribe
  • Request-Reply
  • Queue Groups

1. Publish-Subscribe

Publish와 Subscribe는 각각 메시지를 발행하는 송신자와 메시지를 수신하는 수신자로 봐도 좋을 것 같습니다.

Publish가 message를 발행할경우 Publisher와 매핑되는 토큰을 가진 Subscriber들이 message들을 읽어 처리하는 식입니다.

주고 받을 수 있는 Message는 아래의 필드들로 구성되어있습니다.

  • 과목
  • 패이로드(byte Array)
  • 헤더 필드의 수
  • reply(optional)

Message사이즈는 max_payload설정으로 설정이 가능하고, 기본적으로 1MB가 할당되어있습니다.
최대 사이즈 64MB까지 설정할 수 있지만 무작정 메모리 크기를 늘리기 보다는 꼭 필요한 만큼만 사이즈를 늘려 관리하는 것을 권장하고 있습니다.

2. Request-Reply


요청과 응답 부분입니다.
사용자 즉 Client가 서버에 요청을 보낼경우 서버는 보인이 처리해야하는 로직들을 처리 후 Message를 Publish합니다.

이때, 동기방식으로 Message처리 후 응답을 기다렸다 Client에게 응답을 하는 것이 아닌
서버에서 처리할 부분이 완료될경우 Message는 Publish한 후 Client에게 응답을 하고,
나머지 Message를 받아 처리하는 부분은 Background로 처리합니다.

3. Queue Groups


NATS의 로드밸런싱 기능입니다. 이는 분산 queue기능을 통해 처리하며 이것은 동일한 그룹 내에서 한번만 처리되도록 보장하기 위한 기능입니다.

Queue Groups는 다음과 같은 특징을 가지고 있습니다.

  1. Load Balancing
  2. Message Delivery Guaratees
  3. Scalability
  4. Fallover

위와 같은 특징들은 모두 Message를 동일한 Queue Group내에서 한번만 처리되도록 하기 위해 분산 처리, 전달 보장, 장애 복구의 기능을 통해 메세지가 처리되도록 보장하고 있습니다. 또한 위와 같이 분산처리를 할 경우 K8s 환경에서 개발을 할경우 Pod를 얼마나 늘리든지 상관없이 얼마든지 확장 강능하다는 강점을 가지고 있습니다.

JetStream

NATS server에는 NATS의 기능보다 확장된 기능들을 제공하는 built-in distributed persistence system이 존재하는데 그것이 JetStream입니다.

JetStream에서 제공하는 기능은 다음과 같습니다.

  • Streaming
  • Replay policies
  • Retention policies and limits
  • Limits
  • Retention policy
  • Subhect mapping transformations
  • Persistent and Consistent distributed storage
  • Stream replication factor
  • Mirroring and Sourcing between streams
  • De-coupled flow control
  • Exactly once semantics
  • Consumers
  • Fast push consumers
  • Horizontaliy scalable pull consumers with batching
  • Cosumer acknowledgments
  • Key Value Store

이러한 기능들은 Option이므로 사용하실 때 필요한 기능들을 찾아 적용하시는 것이 좋을 것 입니다.

또한 NATS는 가볍고 빨라야하기 때문에 Event의 Message 즉 Payload가 적정선 이상으로 커지는 것을 경계하는 것으로 보입니다. Message를 설계를 할 때 Message의 크기를 8MB로 제한하고 설계한다면 NATS의 성능을 제대로 활용할 것 같습니다.


참고자료: https://docs.nats.io

728x90
728x90

좋은 아침입니다.

 

이번 post내용은 제목에서 보이듯이 ApplicationContext에서 지원해 주는 getBean이란 메소드를 사용하여 Bean들을 다루는 것에 대해 포스팅 할 예정이다.

 

우리가 업무를 진행할 때 가끔(?) 이런 요구 사항을 받을 수 있다.

 

1. 작업 내용이 몇개가 추가 될 지 모른다.

2. 업무의 종류는 같지만 하는 일은 다를 수도 있다.

 

즉 몇개의 항목이 있는지도 모르고, 추가되는 항목이 있을 때 그 항목이 하는 일도 다를 수 있다는 뜻이다...

 

만약 저런 요구조건이 들어오게 된다면 우리는 if 혹은 switch를 사용하여 코드를 더럽힐 수도 있다.( 필자가 그랬다... )

 

하지만 세상은 바보가 아니고 그를 좀더 합리적으로 해결 할 수 있는 방법은 이미 알게모르게 제공을 해주고 있다.

 

바로 추상화 즉 Interface를 사용하는 것이다.

 

읭...? 갑자기 Interface?? 라고 생각할 수도 있다.

하지만 그렇게 생각한다면 Interface 무엇인가 부터 고민을 해봐야한다 생각한다.

 

Interface가 무엇인가?? 바로 어떤 행위를 규칙으로 정해두고 구현체들에게 해당 행위를 정의하도록 하는 것이 Interface의 역할이다 라고 볼 수 있다고 생각한다.

 

그러니 위처럼 끝이 확실하지 않고 확장을 할 가능성이 있는 요구조건이 들어오면 행위들을 Interface로 설계를 하고 추가 생성을 해야한다면 해당 업무를 담당하는 Class를 만들어 구현하기만 하면 되어 기존 코드에 영향력을 끼치지 않을 수 있다는 장점도 추가로 가져갈 수 있다.

 

이제 Interface를 왜 사용하는지도 알았으니 본문 Bean들을 불러오는 것에 대해 말해보도록 하겠다.

 

고맙게도 Spring에서 지원하는 ApplicationContext에는 Class type을 가지고 있는 Bean들의 이름을 가져올 수 있는 메소드를 지원해준다.

 

우선 사용할 Interface를 만들어준다.

public interface WorkGroup {
    void work();
}

 

이후 해당 Interface를 구현한 Class들을 만들어주면 준비는 끝이다.

그럼 이제 Bean들의 이름을 통해 Bean들을 가져오는 코드를 만들면 된다.

@Component
public class BeanHandler {
    //
    private final List<WorkGroup> works;

    public BeanHandler(ApplicationContext applicationContext) {
        //
        this.works = new ArrayList<>();

        String[] beanNames = applicationContext.getBeanNamesForType(WorkGroup.class);
        for (String beanName: beanNames) {
            this.works.add((WorkGroup) applicationContext.getBean(beanName));
        }
    }
}

getBeanNamesForType()메소드를 사용할 경우 Interface의 class type을 명시해주면 해당 interface를 구현한 구현체들의 Bean이름을 가져올 수 있다.

 

이름을 가져왔다면 applicationContext.getBean() 메소드를 사용하여 해당 이름을 가진 Bean을 가져와 주면 된다.

 

이제 모든 준비가 끝났다 안에서 각자 다른 업무를 진행하는 Class들이지만 우리는 해당 업무를 work()라 정의하여 interface를 만들었고 각각 Class들은 interface를 구현한 구현체다 보니 어떤 Class든 work()메소드를 호출하면 각자 필요에 맞는 업무를 진행할 것이다.

@Component
public class BeanHandler {
    //
    private final List<WorkGroup> works;

    public BeanHandler(ApplicationContext applicationContext) {
        //
        this.works = new ArrayList<>();

        String[] beanNames = applicationContext.getBeanNamesForType(WorkGroup.class);
        for (String beanName: beanNames) {
            this.works.add((WorkGroup) applicationContext.getBean(beanName));
        }
    }

    public void process() {
        //
        log.info("Process Start");
        for (WorkGroup workGroup: works) {
            workGroup.work();
        }
        log.info("Process End");
    }
}

이로써 각자 업무 내용이 다르더라도 process가 시작되면 각각의 그룹들이 일을 하는 것을 지금은 log를 통해 확인해 볼 수 있다.


요구사항은 우리 개발자들이 원하듯이 딱 정해져서 오는 것 보다 유동적인 변화를 가진 요구사항이 오는 경우가 제 짧은 개발 인생에는 더 많은거 같다.

 

그랬을 때 당황하지 않고 이전의 해결방법들을 참고하여 더 좋은 해결방안을 내는게 좋을거 같다.

 

이글 이 도움이 되길 바라며 모두 화이팅 하길 바란다. 이상!

728x90

'Server' 카테고리의 다른 글

REST API란?  (0) 2024.04.14
[Docker] 도커로 데이터 베이스 편하게 사용하자~  (1) 2024.04.08
[Docker] Docker 설치하기  (0) 2024.04.08
[Docker] Docker란 무엇인가?  (2) 2024.04.08
NATS란?  (0) 2024.04.08

+ Recent posts