문제 발생 상황

npx expo start --web


***


TypeError: fetch failed
    at node:internal/deps/undici/undici:13502:13
    at processTicksAndRejections (node:internal/process/task_queues:105:5)

Metro Bundler가 시작되지만, 네트워크 요청을 처리하지 못하고 종료됨

 

문제 원인 분석

1️⃣ Expo가 내부적으로 HTTPS 요청을 보낼 때 SSL 인증 문제 발생

  • Expo가 네트워크 요청을 보내서 필요한 데이터를 가져와야 하는데 실패
  • 특히 Windows 환경에서는 TLS 인증 관련 오류가 발생할 가능성 있음

2️⃣ 네트워크 문제는 아님

  • 브라우저에서 https://exp.host/ 접속 시 정상적으로 열림
  • 인터넷 연결 문제 없음

3️⃣ 방화벽/프록시 영향 가능성

  • VPN 사용 여부 확인 (사용 중지)
  • Windows 방화벽이 Expo 요청을 차단할 가능성 있음

해결 과정

.env 파일 생성 및 NODE_TLS_REJECT_UNAUTHORIZED=0 설정

  1. 프로젝트 루트(fullStackProject1-front/)에 .env 파일 생성
  2. .env 파일에 다음 내용 추가
  3. Expo 실행

 

해결 방법 정리

  1. .env 파일 생성 후 NODE_TLS_REJECT_UNAUTHORIZED=0 설정 (최고 효과적)
  2. Expo CLI 최신 버전 업데이트 (npm install -g expo-cli)
  3. Expo 캐시 삭제 후 재시작 (npx expo start --clear)

'웹개발 > 프론트엔드' 카테고리의 다른 글

JAVASCRIPT Promise  (0) 2022.08.09
REACT componentDidMount  (0) 2022.08.09
REACT 프로젝트 생성  (0) 2022.08.03
프론트엔드란  (0) 2022.08.03

📌 Git을 활용한 형상 관리 예제 코드

Git의 올바른 워크플로우는 기능별 브랜치를 생성하고 코드 리뷰 후 병합(Merge)하는 방식이다.
이 방식을 Git Flow 또는 GitHub Flow라고 부르며, 협업 시 가장 많이 사용됨.


🔹 1. 기능별 브랜치 생성 (Feature Branch)

 
# 기능 개발을 위한 새로운 브랜치 생성 
git checkout -b feature/new-login

📌 설명:

  • feature/new-login 브랜치를 생성하여 로그인 기능을 개발하는 브랜치.
  • 기존 main 브랜치에서 직접 개발하지 않고, 기능별 브랜치를 따로 관리.

🔹 2. 작업 후 변경 사항 커밋

 
# 변경 사항을 스테이징 (추적할 파일 추가) 
git add . 

# 커밋 (의미 있는 커밋 메시지 작성) 
git commit -m "feat: 로그인 페이지 UI 구현"

📌 설명:

  • git add . → 변경된 모든 파일을 스테이징
  • git commit -m "feat: 로그인 페이지 UI 구현" → 명확한 커밋 메시지 작성 (feat, fix, refactor 등 사용)

🔹 3. 원격 저장소에 푸시

# 원격 저장소(GitHub, GitLab 등)로 브랜치 푸시 
git push origin feature/new-login

📌 설명:

  • 원격 저장소로 브랜치를 업로드하여 다른 팀원들과 공유할 수 있음.

🔹 4. Pull Request (PR) 생성 & 코드 리뷰

  • GitHub, GitLab에서 PR(Pull Request) 생성 후 코드 리뷰 요청
  • 리뷰어(팀원, 리드 개발자)가 코드 검토 후 수정 요청 가능

Pull Request 예제

# Pull Request 제목 feat: 로그인 페이지 UI 구현 
# 변경 사항 요약 - 로그인 폼 스타일링 추가 
- 이메일, 비밀번호 입력 필드 구현 
- 로그인 버튼 클릭 이벤트 처리 

# 테스트 방법 
1. `/login` 페이지로 이동 
2. 이메일과 비밀번호 입력 후 버튼 클릭 시 콘솔 출력 확인
 

🔹 5. 리뷰 후 main 브랜치에 머지

# main 브랜치로 이동 
git checkout main 

# 최신 코드 반영 
git pull origin main 

# 기능 브랜치를 병합 
git merge feature/new-login

📌 설명:

  • 코드 리뷰가 완료되면 main 브랜치로 이동 후 병합(Merge).
  • Pull Request 기반으로 머지하면 GitHub에서 자동 처리됨.

GitHub에서 PR을 머지할 때 "Squash & Merge" 방식을 사용하면 커밋 히스토리를 정리 가능.


🔹 6. 머지 완료 후 브랜치 삭제

 
# 로컬 브랜치 삭제 
git branch -d feature/new-login 

# 원격 브랜치 삭제 
git push origin --delete feature/new-login

📌 설명:

  • 기능 개발이 완료되었으므로 기능 브랜치 정리.

🔹 결론

Git 협업에서는 기능별 브랜치를 생성하고, 코드 리뷰 후 main 브랜치에 병합하는 것이 가장 효율적인 방법.
GitHub Flow, Git Flow 등의 워크플로우를 활용하여 버전 관리 체계를 유지하는 것이 중요.
Pull Request(PR)를 활용하여 팀원 간 코드 품질을 유지하고 협업을 원활하게 진행.

제목: OAuth2 + JWT 코드에서 /api/admin/** 경로에 접근하지 못했던 이유와 해결 과정


문제 정의

Spring Boot 애플리케이션에서 OAuth2 인증과 JWT 기반의 권한 관리를 설정하면서, ROLE_ADMIN 권한을 가진 사용자가 /api/admin/** 경로에 접근할 수 없다는 문제가 발생했습니다. 이번 포스팅에서는 이 문제의 원인을 찾고, 해결하는 과정을 공유하고자 합니다.


문제 발생: /api/admin/** 경로에 접근하지 못하는 이유

OAuth2 인증 후 사용자가 ROLE_ADMIN 권한을 가지고 있음에도 불구하고, /api/admin/** 경로에 접근할 때 403 Forbidden 응답이 반환되었습니다. 다음은 이 문제를 해결하기 위한 디버깅 과정과 해결 방법에 대한 기록입니다.

1. 문제 증상

로그를 통해 문제를 확인한 결과, 사용자가 ROLE_ADMIN 권한을 가지고 있음에도 불구하고, Spring Security에서 해당 권한을 인식하지 못하고 있었습니다.

 

2024-06-23 19:55:27 [http-nio-8080-exec-10] DEBUG o.s.security.web.FilterChainProxy - Securing POST /api/admin/announcements
2024-06-23 19:55:27 [http-nio-8080-exec-9] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
2024-06-23 19:55:27 [http-nio-8080-exec-9] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8080/login

로그에서 AnonymousAuthenticationFilter가 활성화되었고, SecurityContext가 익명으로 설정된 것을 확인할 수 있었습니다.

2. 문제 원인 분석

다양한 점검을 통해 다음과 같은 문제 원인들을 발견했습니다:

  • SecurityContext가 올바르게 설정되지 않음: 인증된 사용자의 정보가 SecurityContext에 제대로 설정되지 않았기 때문에, 사용자가 익명으로 처리되었습니다.
  • OAuth2 인증 후 JWT 사용: OAuth2 인증 후, 프론트엔드와의 통신은 커스텀 JWT 토큰을 사용하고 있었습니다. 이로 인해 OAuth2 인증 정보가 실제 사용되지 않았습니다.
  • JWT 필터 설정의 불완전: JWT를 기반으로 한 권한 부여 로직이 SecurityContext에 제대로 반영되지 않았습니다.

문제 해결: /api/admin/** 경로 접근 문제

문제를 해결하기 위해 다음과 같은 단계들을 거쳤습니다:

1. SecurityContext 설정 확인

먼저, OAuth2 로그인 후 SecurityContext에 사용자 인증 정보가 올바르게 설정되고 유지되는지 확인했습니다. 이를 위해 AuthSuccessHandler에서 JWT 토큰을 생성하고, 이를 기반으로 SecurityContext를 설정하도록 수정했습니다.

 

//oauth2 로그인 성공 핸들러
@Component
public class AuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final UserService userService;

    public AuthSuccessHandler(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        if (authentication instanceof OAuth2AuthenticationToken) {
            User user = userService.findUserByOAuthToken((OAuth2AuthenticationToken) authentication);

            // 로그인 성공시 사용자 정보를 기반으로 JWT 토큰 생성
            String token = userService.generateTokenForUser(user);

        }
    }
}

2. JWT 필터 설정 및 권한 부여

JWT 토큰을 사용하여 프론트엔드와의 통신을 처리하면서, Spring Security가 이를 인식하고 적절한 권한을 부여하도록 JwtTokenFilter를 수정했습니다.

 

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserService userService;

    public JwtTokenFilter(JwtService jwtService, UserService userService) {
        this.jwtService = jwtService;
        this.userService = userService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        SecurityContext context = SecurityContextHolder.getContext();
		//스프링 시큐리티 콘텍스트에서 인증정보를 가지고 있지 않으면 인증정보 추가
        if (context.getAuthentication() == null) {
            String token = request.getHeader("Authorization");
            if (token != null && token.startsWith("Bearer ")) {
                token = token.substring(7); // "Bearer " 접두어 제거

                try {
                    // 토큰 유효성 검사 및 클레임 추출
                    Claims claims = jwtService.validateTokenAndExtractClaims(token);

                    // 클레임에서 사용자 ID 추출
                    String userId = claims.getSubject();
                    
                    // 사용자 정보 조회
                    User tokenUser = userService.findUserById(userId);

                    // 사용자 엔티티에서 권한 정보 추출
                    List<GrantedAuthority> authorities = tokenUser.getRoles().stream()
                            .map(User.Role::name)
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());

                    // 새로운 Authentication 객체 생성
                    Authentication authentication = new UsernamePasswordAuthenticationToken(
                            userId, null, authorities);

                    // SecurityContext에 Authentication 객체 저장
                    context.setAuthentication(authentication);
                } catch (Exception e) {
                    // 토큰 검증 실패 처리
                    SecurityContextHolder.clearContext();
                    logger.error("JWT 토큰 검증 실패: {}", e.getMessage());
                }
            }
        }

        filterChain.doFilter(request, response);
    }
}

3. Spring Security 설정 조정

JWT 필터가 작동하도록 Spring Security 설정을 수정했습니다. JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가하여, JWT가 유효한 경우 적절한 권한을 부여하도록 했습니다.

 

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(corsCustomizer()) // CORS 설정 적용
        .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
        .authorizeHttpRequests(authz -> authz
            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS 요청 허용
            .requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN") // '/api/admin' 경로는 ROLE_ADMIN 권한을 가진 사용자만 접근 허용
            .anyRequest().permitAll() // 그 외 모든 요청은 로그인 여부에 관계없이 허용
        )
        .oauth2Login(oauth2 -> oauth2
            .successHandler(authSuccessHandler) // 로그인 성공 핸들러
            .failureUrl("/loginFailure") // 로그인 실패 시 이동할 URL
            .userInfoEndpoint(userInfo ->
                userInfo.userService(customOAuth2UserService) // 사용자 정보를 로드하는 서비스
            )
        )
        //JwtTokenFilter가 UsernamePasswordAuthenticationFilter보다 먼저실행돼야함. (addFilterBefore)
        .addFilterBefore(new JwtTokenFilter(jwtService, userService), UsernamePasswordAuthenticationFilter.class);
    
    return http.build();
}

 

결론

이번 포스팅에서는 /api/admin/** 경로에 접근하지 못했던 이유를 분석하고, 해결하는 과정을 다루었습니다. OAuth2 인증 후 JWT 토큰을 사용하여 프론트엔드와 소통하고, Spring Security에서 권한을 부여하는 방법을 통해 문제를 해결할 수 있었습니다.

  1. SecurityContext 설정 확인: OAuth2 로그인 후 SecurityContext에 인증 정보가 제대로 설정되고 유지되는지 확인했습니다.
  2. JWT 필터 설정: JWT 토큰을 검증하고 SecurityContext에 권한을 부여하는 필터를 구성했습니다.
  3. Spring Security 설정 조정: Spring Security의 필터 체인에서 JWT 필터가 적절하게 작동하도록 설정을 조정했습니다.

이러한 해결 과정을 통해 애플리케이션의 인증 및 권한 부여를 보다 견고하게 구축할 수 있었습니다. 여러분의 프로젝트에서도 비슷한 문제가 발생했을 때, 이 글이 도움이 되기를 바랍니다.

소개

안녕하세요! 이번 포스트에서는 AWS EC2와 Docker를 사용하여 React 애플리케이션을 배포하는 과정에서 발생한 404 오류 문제를 해결하는 방법을 공유하고자 합니다. 특히, Nginx 설정 파일이 React 프로젝트 내부에 있을 때만 성공하고, Nginx를 React 프로젝트에 포함시키지 않았을 때 실패했던 이유를 중점적으로 다룹니다.

1. 문제 상황: Nginx에서 React 앱의 404 오류

1.1 404 오류란?

404 오류는 웹 서버가 요청된 페이지를 찾을 수 없음을 나타냅니다. React와 같은 Single Page Application(SPA)에서는 모든 경로 요청을 하나의 HTML 파일로 처리합니다. 그러나 서버 설정이 이를 고려하지 않으면 경로 요청 시 404 오류가 발생할 수 있습니다.

1.2 SPA와 클라이언트 측 라우팅

SPA는 모든 페이지 로드가 하나의 HTML 파일을 통해 이루어집니다. 페이지 간 전환은 클라이언트 측에서 JavaScript로 관리되기 때문에, 브라우저의 경로 요청은 서버가 아닌 클라이언트에서 처리해야 합니다. 서버가 이 요청을 적절히 처리하지 못하면 404 오류가 발생합니다.

2. Nginx 설정 파일을 React 프로젝트에 포함시키기

2.1 Nginx란?

Nginx는 고성능 웹 서버로, 정적 파일을 서빙하고, 리버스 프록시 및 로드 밸런싱 기능을 제공합니다. React와 같은 SPA를 서빙할 때 주로 사용됩니다.

2.2 Nginx 설정 파일을 React 프로젝트 내부에 포함시키기

React 프로젝트 내부에 Nginx 설정 파일을 포함시키면, Dockerfile에서 이 파일을 쉽게 참조하고 Nginx 설정을 적용할 수 있습니다. 다음은 Nginx 설정 파일을 React 프로젝트에 포함시키는 예시입니다.

 

server {
    listen 80;
    server_name 내도메인

    root /usr/share/nginx/html; # React 정적 파일 경로

    location / {
        try_files $uri $uri/ /index.html; # SPA 라우팅 문제 해결
        autoindex off; # 디렉토리 인덱스 나열하지 않기
    }

    error_page 404 /404.html;
    location = /404.html {
        root /usr/share/nginx/html; # 404 에러 페이지의 실제 경로
        internal;
    }
}

 

2.3 Dockerfile에서 Nginx 설정 파일 참조

Nginx 설정 파일을 React 프로젝트 내부에 포함시켰을 때, Dockerfile에서 이를 참조하는 방식입니다. Dockerfile은 빌드 과정에서 Nginx 설정 파일을 Nginx의 설정 디렉토리로 복사합니다.

 

# Build stage
FROM node:16 AS build-stage
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build

# Production stage
FROM nginx:1.21.0-alpine AS production-stage
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build-stage /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

 

3. Nginx를 React 프로젝트에 포함시키지 않은 경우 발생한 문제

3.1 문제 설명

Nginx를 React 프로젝트에 포함시키지 않고 Docker Compose에서 Nginx와 React를 별도로 관리했을 때, React 프로젝트가 Nginx 컨테이너 내에 정상적으로 복사되지 않는 문제가 발생했습니다. 이는 volumes 설정이 올바르게 적용되지 않았거나, Docker Compose 설정 파일에서 경로 참조가 정확하지 않아서 발생할 수 있습니다.

3.2 Nginx 설정 파일을 Docker Compose에 따로 추가했을 때의 설정

아래는 React 프로젝트와 Nginx를 별도의 컨테이너로 관리하는 docker-compose.yml 파일입니다.

 

services:
  nginx:
    image: nginx:1.21.0-alpine
    ports:
      - "80:80" # 외부 포트 80을 Nginx의 80 포트로 매핑
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf # Nginx 설정 파일
      - ./react-app/build:/usr/share/nginx/html # React의 정적 파일 디렉토리 매핑
    depends_on:
      - db
      - springboot-app
    networks:
      - my-network

  react-app:
    image: kkm9291/react-app:latest
    expose:
      - "3000" # React 앱의 내부 포트
    networks:
      - my-network

이 설정에서 Nginx는 default.conf 설정 파일과 React의 빌드 파일을 자신의 컨테이너로 복사합니다.

3.3 발생한 문제의 원인

Nginx 설정 파일이 React 프로젝트 외부에 있으면 다음과 같은 문제들이 발생할 수 있습니다:

  • 경로 문제: Docker Compose에서 volumes 경로가 잘못 참조되거나, 상대 경로가 올바르게 설정되지 않아 Nginx가 React의 정적 파일을 찾지 못할 수 있습니다.
  • 타이밍 문제: Nginx 컨테이너가 React 컨테이너보다 먼저 시작되거나, React 컨테이너가 빌드되기 전에 Nginx가 설정을 적용하려고 시도하면 문제가 발생할 수 있습니다.
  • 권한 문제: Docker가 호스트 파일 시스템에서 컨테이너로 파일을 복사하는 과정에서 권한 문제가 발생할 수 있습니다. 특히, 컨테이너가 파일을 읽거나 쓸 권한이 없으면 문제가 발생할 수 있습니다.

4. 문제 해결 후 결과 확인

4.1 Nginx 컨테이너 내부 확인

Nginx 컨테이너 내부의 파일 구조를 확인하여 React 애플리케이션이 올바르게 배포되었는지 확인했습니다.

 

docker exec -it 앱이름-nginx-1 /bin/sh
cd /usr/share/nginx/html
ls -l

 

이 명령어를 통해 빌드된 React 파일들이 Nginx의 정적 파일 디렉토리에 존재하는 것을 확인할 수 있었습니다.

 

결론

이 포스트에서는 AWS EC2와 Docker를 사용하여 React 애플리케이션을 배포하는 과정에서 발생한 404 오류를 해결하는 방법을 다뤘습니다. Nginx 설정 파일의 위치가 React 프로젝트 내부에 있을 때만 성공한 이유와, Nginx를 React 프로젝트에 포함시키지 않았을 때 발생한 문제의 원인을 설명했습니다. 

pring Boot 애플리케이션에서 API 전용으로 설정했음에도 불구하고, /favicon.ico 요청으로 인해 Circular view path [favicon] 오류가 발생하는 경우가 있습니다. 이 오류는 Spring Boot가 favicon.ico 요청을 처리하려고 시도할 때 적절한 핸들러를 찾지 못해 발생합니다.

이 포스팅에서는 이 문제를 해결하기 위해 사용한 두 가지 접근 방법에 대해 설명하겠습니다.

문제 설명

Spring Boot 애플리케이션에서 /favicon.ico에 대한 GET 요청이 들어오면, 이를 처리할 핸들러가 없을 때 Circular view path 오류가 발생할 수 있습니다. 이 오류는 기본적으로 뷰 리졸버가 요청을 처리하려고 시도하지만, 이를 반복해서 호출하게 되어 발생합니다.

jakarta.servlet.ServletException: Circular view path [favicon]: would dispatch back to the current handler URL [/favicon] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
 

이 문제를 해결하기 위해서는 favicon.ico 요청을 적절히 처리하거나 무시하도록 설정해야 합니다.

해결 방법

문제를 해결하기 위해 다음 두 가지 방법을 사용했습니다:

  1. 정적 리소스 핸들러 설정
  2. /favicon.ico 요청을 처리하는 Controller 추가

1. 정적 리소스 핸들러 설정

Spring Boot에서는 WebMvcConfigurer를 사용하여 정적 리소스를 제공할 수 있는 경로를 설정할 수 있습니다. 정적 리소스 핸들러를 설정함으로써, /static/** 경로의 리소스 요청이 classpath:/static/ 디렉토리에서 제공되도록 할 수 있습니다.

아래는 정적 리소스 핸들러를 설정한 코드입니다:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}
 
 

이 설정을 통해 애플리케이션은 /static/ 경로의 정적 리소스를 classpath:/static/에서 제공하게 됩니다. 그러나 /favicon.ico 요청은 여전히 처리되지 않기 때문에, 다음 단계에서 이를 처리하는 Controller를 추가해야 합니다.

2. /favicon.ico 요청을 처리하는 Controller 추가

/favicon.ico 요청에 대해 204 No Content 응답을 반환하도록 설정하여, Spring Boot가 이 요청을 적절히 처리하도록 할 수 있습니다. 이를 위해 간단한 Controller를 추가합니다:

 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}

 

이 Controller는 /favicon.ico 요청이 들어올 때 빈 응답을 반환하며, HTTP 상태 코드 204 (No Content)를 설정합니다. 이를 통해 순환 참조 오류 없이 안전하게 요청을 무시할 수 있습니다.

최종 결과

위의 두 가지 설정을 적용한 후, /favicon.ico 요청으로 인해 발생하던 Circular view path 오류는 더 이상 발생하지 않게 되었습니다. 이 과정에서 우리는 /favicon.ico 요청을 무시하거나 적절히 처리하는 방법을 배웠으며, Spring Boot 애플리케이션에서 API와 정적 리소스 경로를 명확히 구분할 수 있게 되었습니다.

요약

Spring Boot에서 /favicon.ico 요청으로 인해 발생하는 오류를 해결하기 위해 다음 두 가지 방법을 적용했습니다:

  1. 정적 리소스 핸들러 설정: WebMvcConfigurer를 사용하여 정적 리소스를 특정 경로에서 제공하도록 설정했습니다.
  2. /favicon.ico 요청을 처리하는 Controller 추가: /favicon.ico 요청이 들어왔을 때 204 No Content 응답을 반환하는 Controller를 추가했습니다.

이 해결 방법은 Spring Boot 애플리케이션에서 API 전용 설정을 유지하면서 불필요한 오류를 방지하는 데 매우 유용합니다. 앞으로 비슷한 문제가 발생할 때 이와 같은 방법을 적용해 보세요.

문제 상황

Docker Compose를 사용하여 Spring Boot 애플리케이션과 MySQL 데이터베이스를 함께 배포하려 했습니다. docker-compose up 명령을 실행하자, Spring Boot 애플리케이션이 데이터베이스 연결에 실패하며 시작하지 못하는 문제가 발생했습니다. 로그를 확인한 결과, Spring Boot 애플리케이션이 MySQL 데이터베이스가 준비되지 않은 상태에서 기동을 시도하고 있었던 것을 알게 되었습니다. 이는 데이터베이스가 준비되기 전에 애플리케이션이 시작되면서 연결을 시도해 실패한 것이 원인이었습니다.

cohttp://m.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.

...

org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is org.apache.commons.dbcp2.SQLNestedException: Cannot create PoolableConnectionFactory (Communications link failure)

...

java.net.ConnectException: Connection refused (Connection refused)
at java.base/sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:779)

 

해결 방법

이 문제를 해결하기 위해 Docker Compose의 depends_on 및 healthcheck 기능을 사용하여 데이터베이스가 완전히 준비될 때까지 Spring Boot 애플리케이션이 시작되지 않도록 설정했습니다. healthcheck를 통해 데이터베이스의 상태를 모니터링하고, 준비 상태를 확인한 후에만 Spring Boot 애플리케이션이 기동되도록 했습니다.

 

version: '3.8'

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: mypassword
      MYSQL_DATABASE: mydatabase
      MYSQL_USER: myuser
      MYSQL_PASSWORD: mypassword
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - my-network
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost -p${MYSQL_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 5

  springboot-app:
    build:
      context: ./springboot
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/mydatabase
      - SPRING_DATASOURCE_USERNAME=myuser
      - SPRING_DATASOURCE_PASSWORD=mypassword
    depends_on:
      db:
        condition: service_healthy
    networks:
      - my-network

volumes:
  db_data:
    driver: local

networks:
  my-network:
    driver: bridge
docker-compose up --build
명령을 사용했을때 발생하는 
failed to solve: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount277479487/Dockerfile: no such file or directory  
오류는,
도커컴포즈 파일에 명시한 경로에서 도커파일을 찾지 못했을때 발생합니다.
먼저 도커파일이 해당 경로에 있는지 확인하고, 대소문자등 틀린 부분이 없나 확인해줍니다.
그래도 문제가 해결되지 않을경우,
혹시 도커파일을 새 텍스트파일 만들기 등으로 만들었다가 .txt를 지워버린 형태로 생성하지는 않았나 생각해봅니다.
해당 파일을 지우고, IDE에서 새 파일 만들기로 도커파일을 다시 만든 후에 컴포즈 up 명령을 실행해봅니다.
 

1. 파일 확장자 문제

.txt로 만들었다가 확장자를 지우는게 안되는 이유:

Windows 탐색기에서 파일을 생성할 때 파일 확장자가 숨겨진 경우가 많습니다. 이로 인해, Dockerfile을 생성할 때 .txt 확장자가 자동으로 추가될 수 있습니다. 사용자가 Dockerfile이라고 이름을 변경해도 실제 파일 이름은 Dockerfile.txt로 남아 있을 수 있습니다.

이 경우, 파일 탐색기에서는 Dockerfile로 보이지만, 내부적으로는 여전히 텍스트 파일로 인식되기 때문에 도커가 이를 Dockerfile로 인식하지 못합니다.

 

2. 파일 형식 문제

텍스트 편집기에서 파일을 저장할 때, 파일의 형식이 중요합니다. Windows의 메모장이나 다른 텍스트 편집기는 기본적으로 파일을 UTF-16 형식으로 저장할 수 있으며, 도커 빌드는 UTF-8 형식을 기대할 때 문제가 발생할 수 있습니다.

다음은 이 문제와 관련된 몇 가지 사항입니다:

  • 텍스트 인코딩: Dockerfile을 UTF-8 형식으로 저장해야 합니다. 많은 IDE는 파일을 기본적으로 UTF-8 형식으로 저장하지만, Windows 메모장 등은 그렇지 않을 수 있습니다.
  • 파일 끝에 숨겨진 문서 헤더: 메모장 등 일부 편집기는 파일 시작 부분에 숨겨진 BOM (Byte Order Mark)을 추가할 수 있습니다. 이것이 도커가 파일을 읽는 데 문제를 일으킬 수 있습니다.

먼저 개념 정리를 하고 들어가겠습니다.

OAuth2란 무엇인가

OAuth2는 사용자 인증 및 권한 부여를 위한 업계 표준 프로토콜입니다. 이를 통해 애플리케이션은 서드 파티 서비스의 기능을 안전하게 사용할 수 있습니다.

 

OAuth2 의존성의 핵심 역할

OAuth2 의존성은 애플리케이션에 인증 및 권한 부여 기능을 쉽게 통합할 수 있도록 지원합니다. 이를 통해 개발자는 복잡한 보안 요구사항을 쉽게 구현할 수 있습니다.

 

스프링 시큐리티의 역할

스프링 시큐리티는 애플리케이션의 보안을 위한 포괄적인 프레임워크를 제공합니다. 이는 인증 및 권한 부여, 보안 헤더 관리, CSRF 보호 등을 관리합니다.

 

YML 파일의 OAuth2 설정

YML 파일에서 OAuth2 설정을 통해 클라이언트 ID, 시크릿, 리다이렉션 URI 등과 같은 필수 인증 정보를 구성할 수 있습니다. 이를 통해 OAuth2 인증 흐름이 애플리케이션에 통합됩니다.

 

인증 성공 후 JWT 토큰 발급 이유

인증 성공 후 JWT 토큰을 발급하는 이유는 서버와 클라이언트 간의 안전한 통신을 보장하기 위함입니다. JWT는 사용자 인증 정보를 포함하며, 이를 통해 후속 요청의 인증을 간소화합니다.

 

이제부터 순서대로 따라 오시면 됩니다.

1. 메이븐 혹은 그래들로 의존성 주입.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

 

2. 원하는 소셜 로그인 개발자 홈페이지에서 앱 등록 후,

클라이언트ID, 클라이언트Secret 발급

원하는 리디렉트URI 설정하고 오기.

 

이 포스트에서는 카카오를 예로 구현하겠습니다.

카카오 개발자 사이트 주소(https://developers.kakao.com/)

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

카카오ID로 로그인 후 ->

 

내 애플리케이션 ->

 

애플리케이션 추가하기 ->

 

왼쪽 요약정보에서 REST API키 메모 후 아래 플랫폼 설정하기 ->

 

애플리케이션에 맞는 플랫폼 선택 후 (모바일이 아니면 Web을 선택하세요) ->

 

내 애플리케이션 주소 입력 (개발환경이면 http://localhost:8080 등) ->

 

다시 왼쪽 메뉴에서 카카오 로그인 클릭 ->

 

활성화 및 리디렉트Uri 설정 ->

(내도메인/login/oauth2/code/kakao)

(이 주소로 접속하면 카카오 로그인 페이지가 나오게 됩니다)

(이 주소는 잠시 후 yml파일에서도 동일하게 작성합니다)

 

왼쪽 메뉴에서 보안 클릭 ->

 

Secret키 발급 후 메모 (활성화는 사용안함 선택하세요)

3. application.yml(혹은 properties) 작성

security:
    oauth2:
      client:
        registration:
          google:
            clientId: 
            clientSecret:
            scope:
              - profile
            redirectUri: http://localhost:8080/login/oauth2/code/google
          kakao:
            clientId: 카카오개발자에서 메모한 RESTApi 키
            clientSecret: 카카오개발자에서 생성한 Secret 키
            redirectUri: "http://localhost:8080/login/oauth2/code/kakao" (개발자에서 설정한것)
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_post
            client-name: Kakao
          naver:
            client-id: 
            client-secret: 
            client-name: Naver
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/naver
        provider: (구글은 필요없고 카카오 네이버는 작성해야함)
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response

 

+ Recent posts