여기에서 web과 database는 각각 하나의 서비스. 이 경우, 각 서비스는 각각 하나의 컨테이너를 생성하지만, 필요에 따라 한 서비스에서 여러 컨테이너를 생성할 수도 있습니다 (예: 스케일링 목적으로).
결국, "서비스"는 Docker Compose에서 응용 프로그램의 한 부분 (예: 웹 서버, 데이터베이스)을 나타내며, 이 서비스를 구성하는 데 필요한 설정과 함께 도커 이미지 정보를 포함합니다.
사용방법
docker-compose build:
이 명령어는 docker-compose.yml 파일에 정의된 모든 서비스의 이미지를 빌드합니다. docker-compose.yml 파일에서 build: 섹션을 사용하여 Dockerfile의 위치나 빌드 컨텍스트 등을 지정한 서비스에 대해서만 이 명령어를 사용해야 합니다.
docker-compose up -d:
위에서 빌드한 이미지들의 실행 인스턴스인 컨테이너들을 만들어 실행 -d 옵션은 "detached mode"를 의미하며, 백그라운드에서 컨테이너를 실행하라는 것을 의미 이미지가 없다면 빌드도 시도하기 때문에 docker-compose build 없이 이것만 실행해도 되긴 하나,
코드 변경시 재빌드해주진 않기 때문에 코드 변경이 있었다면 build후 up해야 한다.
docker-compose down -v:
up과 반대로 컨테이너들 중단하고 없애준다.
-v하면 관련 데이터 볼륨도 제거해준다.
여기서 데이터 볼륨(Data Volume)은 Docker 컨테이너 파일 시스템의 일부를 호스트 시스템에 저장하는데 사용되는 매커니즘으로, 컨테이너간 데이터를 공유하는 공유폴더 개념으로 쓰이거나 데이터를 영구적으로 저장할수 있게 해준다.
그 예시로는 (DB, 로그, config file등이 있다)
docker-compose ps
docker-compose로 관리되는 컨테이너들의 현재 상태와 관련된 정보를 조회
다음내용 확인가능
서비스 상태 확인: docker-compose up 명령을 사용하여 여러 서비스를 시작한 후, 각 서비스의 컨테이너가 올바르게 실행 중인지, 혹은 중지된 상태인지를 확인하기 위해 사용됩니다.
문제 진단: 서비스에 문제가 발생했을 때, 어떤 컨테이너가 문제를 일으키고 있는지 확인하기 위해 사용될 수 있습니다. 예를 들어, 컨테이너가 계속 재시작되는 경우, docker-compose ps를 통해 해당 컨테이너의 상태를 확인할 수 있습니다.
포트 정보 확인: 각 컨테이너에서 어떤 포트가 호스트와 연결되었는지 확인하려 할 때 사용됩니다.
스케일링 확인: docker-compose up --scale 명령을 사용하여 서비스의 인스턴스 수를 조정한 경우, 현재 실행 중인 인스턴스 수를 확인하기 위해 사용됩니다.
컨테이너 이름 확인: docker-compose로 시작된 컨테이너들은 특정한 네이밍 규칙을 가지고 있습니다. 이 이름을 확인하기 위해 docker-compose ps를 사용할 수 있습니다.
아래는 예시
Name Command State Ports
--------------------------------------------------------------------------------------------------------------------------------------------------
table-walkthrough_data-generator_1 /docker-entrypoint.sh Up
table-walkthrough_grafana_1 /run.sh Up 0.0.0.0:3000->3000/tcp,:::3000->3000/tcp
table-walkthrough_jobmanager_1 /docker-entrypoint.sh stan ... Up 6123/tcp, 0.0.0.0:8082->8081/tcp,:::8082->8081/tcp
table-walkthrough_kafka_1 start-kafka.sh Up 0.0.0.0:9092->9092/tcp,:::9092->9092/tcp
table-walkthrough_mysql_1 docker-entrypoint.sh --def ... Up 3306/tcp, 33060/tcp
table-walkthrough_taskmanager_1 /docker-entrypoint.sh task ... Up 6121/tcp, 6122/tcp, 6123/tcp, 8081/tcp
table-walkthrough_zookeeper_1 /bin/sh -c /usr/sbin/sshd ... Up 0.0.0.0:2181->2181/tcp,:::2181->2181/tcp, 22/tcp, 2888/tcp, 3888/tcp
sevity@sevityubuntu:~/workspace/flink-playgrounds/table-walkthrough$
후자가 Resolver(리졸버)만 구현하면 스키마를 자동생성해주고 kotlin 타입과 연동되는등 kotlin을 사용한다면 더 편리한 측면이 있지만, 여기서는 전자를 사용한 기준으로 서술한다.
expediagroup 아티펙트의 경우, 내 경우엔 /graphql 404문제가 해결이 안돼서 springframework를 쓰기로 했다. 추가로 조사해보니 webmvc대신 webflux로 교체해야 호환되는 이슈도 있었다(여기) spring-boot-starter-web 대신 spring-boot-starter-webflux의존성으로 바꿔야 하는데 WebConfig.kt등 여러곳에서 코딩방식을 바꿔야 하는 걸로 보인다.
클라이언트 사이드(React/Next.js)
관련 library설치
npm install @apollo/client graphql
아폴로?
Apollo Client는 JavaScript 어플리케이션에서 GraphQL API와 통신할 수 있게 해주는 라이브러리입니다.
Facebook에서는 GraphQL을 발명했으며, Relay라는 GraphQL 클라이언트를 만들어 공개했습니다. 그러나 Apollo가 Relay보다 더 널리 사용되는 이유는 사용자 친화적: Apollo는 사용자 친화적이고, 초보자에게 친숙하며, 설정이 상대적으로 간단합니다. 문서화도 잘 되어 있어, 개발자들이 쉽게 접근하고 사용할 수 있습니다. 커뮤니티 지원: Apollo는 강력한 커뮤니티 지원을 받고 있으며, 다양한 추가 기능과 툴이 개발되고 있습니다. 또한 꾸준한 업데이트와 개선이 이루어지고 있어, 더 많은 개발자들이 Apollo를 선호하게 되었습니다.
src/main/resources/graphql/schema.graphqls 파일에 쿼리를 추가
type Query {
submissionCountByProblem(problemId: ID!): Int
}
2. 서비스 수정
SubmissionService에 getSubmissionCountByProblem매서드를 추가하여 문제별 제출 수를 가져옴
@Service
class SubmissionService(private val submissionRepository: SubmissionRepository) {
// 아래는 기존에 존재하던 매서드
fun submitProblem(userId: Long, problemId: Int, code: String): Submission {
val submission = Submission(
userId = userId,
problemId = problemId,
code = code,
status = "PENDING" // 초기 상태
)
return submissionRepository.save(submission)
}
// 아래 매서드 추가
fun getSubmissionCountByProblem(problemId: Int): Int {
return submissionRepository.countByProblemId(problemId)
}
}
3. 컨트롤러에 로직 추가
컨트롤러에@QueryMapping 어노테이션을 사용하여 submissionCountByProblem GraphQL쿼리를 처리하는 메서드를 추가(엔드포인트 추가는 아니지만 약간 유사)
package com.sevity.problemservice.controller
import ...
@RestController
class SubmissionController(private val submissionService: SubmissionService) {
// 기존 코드
// ...
@QueryMapping
fun submissionCountByProblem(@Argument problemId: Int): Int {
return submissionService.getSubmissionCountByProblem(problemId)
}
}
expediagroup 아티펙트의 경우, 위의 해결책을 적용해도 여전히 404가 떴고, 추가로 조사해보니 webmvc대신 webflux로 교체해야 하는 이슈가 있었다(여기) spring-boot-starter-web 대신 spring-boot-starter-webflux의존성으로 바꿔야 하는데 WebConfig.kt등 여러곳에서 코딩방식을 바꿔야 하는 걸로 보인다.
application.properties에서 포트설정해주고(이때 src/main/resources뿐 아니라 src/test/resources에 있는 파일도 해줘야함에 주의)
# in application.properties
grpc.server.port = 50051
다음처럼 gRPC서버 띄욱 listen작업도 해줘야 했다.
package com.sevity.authservice.config;
import ...
@Configuration
public class GrpcServerConfig {
private static final Logger logger = LoggerFactory.getLogger(GrpcServerConfig.class);
@Autowired
private SessionServiceImpl sessionService;
private Server server;
@Value("${grpc.server.port}")
private int port;
@PostConstruct
public void startServer() throws IOException {
server = ServerBuilder
.forPort(port)
.addService(sessionService) // Your gRPC service implementation
.build();
server.start();
logger.info("sevity gRPC server started on port {}", port);
logger.info("sevity gRPC service name: {}", sessionService.getClass().getSimpleName());
}
@PreDestroy
public void stopServer() {
if (server != null) {
server.shutdown();
}
logger.info("sevity gRPC server stopped");
}
}
현재 만들어 보고 있는 online judge 프로젝트의 서비스 구성은 다음과 같다.(관련 있는 2개만 표시. 실제로는 7개)
인증 서비스 (Backend): 사용자의 회원 가입, 로그인, 로그아웃, 세션 관리 등을 담당 인증 서비스 (Frontend): 사용자 인터페이스를 제공 (로그인 폼, 회원가입 폼 등)
한가지 알아두면 좋은점은 Spring Boot의 경우 /login, /logout endpoint의 경우 직접 정의하지 않아도 자동으로 처리한다는 점이다.(이점 때문에 디버깅시 많이 헷갈렸다ㅠ)
세션은 서버에서 브라우저로 set-cookie 헤더를 통해서 세션아이디를 부여한다.
아래처럼 브라우저 개발자 도구에서 확인가능하다(애플리케이션탭 > 쿠키섹션)
서버의 세션과 브라우저(클라이언트)의 쿠키 개념
세션을 서버에서 생성하고 세션id를 set-cookie를 통해서 브라우저(클라이언트)로 전달한다.
이때 쿠키는 브라우저를 종료해도 유지되는 지속쿠키를 쓰고 만료시점을 정의할수도 있고, 세션쿠키를 쓰면 브라우저 종료시 자동으로 쿠키도 삭제된다.
서버의 세션의 경우 지속시간을 application.properties에 다음과 같이 지정할 수 있다.
server.servlet.session.timeout=30m
현재 내가 테스트중인 프로젝트에서는 /login 성공시 세션쿠키가 발급되며, 세션 타임아웃의 효과는 확인이 안되었다.
일단은 이정도에서 더 깊이 안파고 넘어가기로 한다.
트러블슈팅
http에서 포트가 서로다른 서비스(MSA)간 연동하기
아래 크롬 설명에 따르면 https를 쓰고 secure 옵션을 주어야 SameSite=None으로 지정하면서 포트가 달라도 쿠키저장이 된다는것 같다.(관련글, 관련글2)
set-cookie로 응답을 제대로 했음에도 브라우저에 쿠키저장이 안될때
클라이언트에서 서버로 요청할때 credential을 보내줘야 했다(이것땜에 한참헤맴 ㅠ)
const response = await axios.post('https://sevity.com:9991/login', `username=${username}&password=${password}`, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
withCredentials: true, // 여기, 이 줄 없으면 브라우저에 쿠키저장 안된다!!
});
getSession(true)의 안전성
HttpSession session = request.getSession(false); 를 하면 존재하는 세션정보를 가져오고, false자리에 true를 넣으면 없으면 생성하라는 의미인데, 이렇게 하면 (내가만든세션), (SpringSecurity에 의해 아직 없지만 생성될 세션) 이렇게 두벌이 될까봐 우려했는데, 나중에 확인해보니 그렇지는 않았다. 따라서 항상 true로 호출해도 무방한 것 같다.
단 여기를 보면 멀티스레드 환경에서 레이스 컨디션에 의해 중복세션과 중복쿠키가 생성되는 경우는 있는 것 같다. 해결책은 아래처럼 동기화블록내에 생성과 처리를 묶어주면 되긴할듯(현재 내 구현에서는 그렇게 까지 하진 않았다)
synchronized (request) {
HttpSession session = request.getSession(true);
// ... 세션 사용 코드 ...
}
TMI
/login에서 반환되는 response.data 값과 SESSION쿠키의 값은 동일하나, SESSION쿠키의 경우 base64로 인코딩 되어 있다.
//response.data: c8545bb7-c90b-4d69-9128-08efd0a73866
//SESSION(쿠키): Yzg1NDViYjctYzkwYi00ZDY5LTkxMjgtMDhlZmQwYTczODY2
let encodedSessionId = 'Yzg1NDViYjctYzkwYi00ZDY5LTkxMjgtMDhlZmQwYTczODY2';
let decodedSessionId = atob(encodedSessionId);
console.log(decodedSessionId); // 출력: c8545bb7-c90b-4d69-9128-08efd0a73866