프로젝트 - 1주차 (CI/CD , Git convention , Session과 AOP)

YoungSun·2026년 1월 4일

개발 1주차

근데 기획을 곁들인...

이번 주차에는 확정된 기능만 구현하기로 하고 본격적인 개발에 앞서 협업에 필요한 기본 구조를 먼저 정리했다.
이 과정에서 나는 API 설계와 데이터베이스 설계를 맡아 진행했다.

  1. CI/CD?
  2. GitHub 세팅
  3. Session 로그인과 쿠키 로그인
    3-1 세션 로그인 AOP로 구현하기

1.CI/CD

  • CI : Continuous Integration
    • 지속적인 통합, 코드 커밋하면 자동으로 충돌/에러 테스트
  • CD : Continuous Deployment
    • 자동으로 통합된 코드를 사용자 환경에 자동으로 배포함, 자동 업데이트

만약에 쇼핑몰 사이트를 제작하고 있다 생각했을때
검색 기능을 깃허브에 커밋만 하면, 자동으로 사용자 웹 페이지에서 검색 기능을 사용 할 수 있음

지금까지는 수동으로 모든 작업을 하는게 당연하다고 생각했다.
CI/CD 도구를 통해 빌드와 배포 과정을 자동화 한다면 개발 외적인 반복 작업에 사용되는 시간을 줄일 수 있다고 느꼈고 바로 도구들을 찾아봤다

젠킨스 & 깃허브 액션

젠킨스와 깃허브 액션은는 모두 CI/CD 도구지만 서버 관리 방식에서 명확한 차이가 있다.

젠킨스는 사용자가 직접 서버를 구축하고 관리해야 한다.
EC2 인스턴스에 젠킨스를 설치하고 보안 설정/플러그인 관리/에이전트 구성 등을 직접 해야 하므로 초기 설정과 운영 난이도가 높은 편이다.
대신 파이프라인을 자유롭게 구성할 수 있어 복잡한 배포 로직이나 커스터마이징이 필요한 환경에 적하다.

반면 GitHub Actions는 GitHub에서 제공하는 관리형 CI/CD 서비스다.
별도의 서버를 운영할 필요 없이 리포지토리에 워크플로우 파일만 작성하면 바로 사용할 수 있어 인프라 관리 부담이 거의 없다. 다만 GitHub 생태계에 종속되며, 외부 시스템과의 복잡한 연동이나 세밀한 제어에는 한계가 있다.

정리하면:

젠킨스: 서버 직접 관리 필요, 설정은 어렵지만 높은 자유도와 확장성

GitHub Actions: 서버 관리 불필요, 빠른 도입 가능하지만 GitHub 의존적

이 차이 때문에 우리 프로젝트는 GitHub Actions을 추천받았다

젠킨스: Self-hosted 방식입니다. 직접 서버(AWS EC2, 온프레미스 등)를 구축하고 젠킨스를 설치해야 합니다. 서버 사양을 자유롭게 정할 수 있지만, 보안 업데이트나 플러그인 관리를 직접 해야 하는 운영 부담이 큽니다.

깃허브 액션: SaaS(클라우드) 기반입니다. 깃허브가 인프라를 관리하므로 별도의 설치 없이 .github/workflows 폴더에 설정 파일만 넣으면 바로 실행됩니다. 관리가 매우 편하지만, 깃허브 서비스 상태에 의존하게 됩니다.

주영님의 답변
젠킨스는 사용하기 어려운데 깃허브 액션은 사람들이 정의한 action으로 쉽게 설정이 가능하기 때문에
젠킨스보다는 깃허브 액션 사용하는 것을 추천한다.

2.Github 세팅

컨벤션과 템플릿

팀 프로젝트에서 협업 효율을 높이기 위해 GitHub 컨벤션과 소울치킨님이 주신 템플릿을 연습용 깃에 설정해봤다.

각자 다른 방식으로 커밋 메시지나 이슈를 작성하면 기록이 정리되지 않고 변경 이력을 한눈에 파악하기 어려워진다.
이를 방지하기 위해 형식을 통일한다.

커밋 컨벤션은 타입과 바디로 구성 되며 기능 추가 버그 수정 문서 수정 등을 구분함으로써 커밋 로그만 보더라도 변경 내용을 빠르게 파악할 수 있다.

컨벤션과 템플릿 설정은 협업 과정에서 발생하는 시간 낭비와 혼선을 줄이기 위한 최소한의 규칙이다.

gitmessage.txt

.gitmessage.txt 터미널에 출력되는 템플릿이다

.gitmessage.txt 파일을 생성하고

git config  commit.template .gitmessage.txt

를 입력해 모든 사용자에게 적용시킨다

사용법

  1. git add .
  2. git commit
  3. vi 모드로 진입하게 되는데 i,a,o를 누르고 형식에 맞게 입력한다
  4. 작성을 완료했다면 :wq! 로 commit을 완료할 수 있다

하지만 VS Code를 사용하는 입장에서

이게 더 편하다... 안쓸것 같아서 보류

출처 : https://sungwookoo.tistory.com/1
https://duektmf34.tistory.com/206

3.세션과 쿠키

HTTP는 무상태 특성을 가지기 때문에 요청이 끝나면 이전 요청의 상태를 기억 못한다.
그래서 로그인과 같은 사용자 정보를 저장할 수단이 필요하다.
대표적으로 세션과 쿠키다.

쿠키

쿠키는 클라이언트(브라우저)에 저장되는 정보다.
서버가 응답 시 쿠키를 발급하면, 브라우저는 이후 요청마다 해당 쿠키를 함께 전송한다. 구현이 단순하고 서버 부담이 적다는 장점이 있지만, 클라이언트에 저장되기 때문에 보안에 취약하며 저장할 수 있는 정보의 크기에 제한이 있다.

세션

세션은 서버에 사용자 정보를 저장하고, 클라이언트에는 해당 세션을 식별하기 위한 세션 ID만 전달하는 방식이다.
민감한 정보를 서버에서 관리할 수 있어 보안 측면에서는 유리하지만, 사용자가 많아질수록 서버 자원을 사용하게 되어 부하가 발생할 수 있다.

세션 처리 순서

생성과 사용 순서는 다음과 같다

  1. 로그인 성공시 세션 하나를 만든다
    • 암호문 하나를 만들고 사용자 쿠키에 넣어서 전송
    • 사용자 정보는 서버에 저장
  2. 사용자가 다른 요청을 할 때 마다 서버는 암호문 값을 확인하고, 세션 저장소에서 검색함
  3. 해킹 당해도 암호문만 있으니 실제 사용사의 개인정보를 탈취 불가능

그러면 세션을 실시간으로 탈취하고 요청 보내면 되지 않나?
-> 이게 세션 하이재킹

쿠키를 사용하면 2번 Key 값에 사용자 정보를 입력한다

(연습용)세션 생성 코드

HttpSession의 setAttribute를 통해 암호문과 사용자 정보를 저장한다
아래 코드에서는 LOGIN_MEMBER_ID라는 이름의 Id를 저장

setAttribute

public static String setLoginMemberId (
	HttpSession session , String id) {
    
    session.setAttribute("LOGIN_MEMBER_ID", id); 
    
    }

저장 결과

{
  "LOGIN_MEMBER_ID" : "user_Id"
}

세션 확인 코드

   public static String getLoginMemberId(
   HttpSession session) {
        return (String) session.getAttribute(LOGIN_MEMBER_ID);
    }

아래부터 잘못된 정보가 매우 많습니다.

3-1 세션로그인 AOP 구현

세션 로그인 검증은 하나의 기능이지만 거의 모든 API에서 반복적으로 필요하다.
컨트롤러마다 직접 로그인 여부를 확인하는 방법은 코드 중복을 만드므로 AOP를 사용해 로그인 검증을 한번에 처리했다.

커스텀 어노테이션 (@LoginCheck)

세션 로그인 검증을 AOP로 분리하더라도
모든 컨트롤러 메서드에 적용하는건 불필요하다
(그럴일은 없겠지만 컨트롤러 어노테이션을 수정할 수 있나?)
아무튼,
필요한 메서드에만 적용하면 되므로
커스텀 어노테이션@LoginCheck을 만들었다.

@Retention(RetentionPolicy.RUNTIME)

해당 어노테이션은 런타임 시점까지 유지되도록 설정 했다

@Target(ElementType.METHOD)

컨트롤러 메서드 단위로 로그인 검증을 적용하기 위해
메서드에만 사용할 수 있도록 제한했다.

public @interface LoginCheck

사용할 어노테이션 인터페이스를 추가한다.
@LoginCheck 마커 역할 뿐만 아니라
사용자 권한을 함께 검사할 수 있도록 열거형을 포함해서,
역할 기반 제어 구조로 확장할 수있도록 했다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoginCheck {

	public static enum UserType{
    	USER,ADMIN
    }
	UserType type();
}

AOP를 빈으로 등록하기 위한 클래스 작성

클래스 단위 어노테이션

@Aspect : 나 Aspect 부분으로 사용할 로직임 !
@Component : 빈으로 등록하겠음 !

AOP 관련 어노테이션

Aspect가 적용될 어노테이션의 범위 설정 가능

@Around : 해당 Aspect 어노테이션이 사용되는 ElementType에서 실행 전, 실행 후에 동작하겠다고 명시함.
++ @Before , @After

@Around 내부 이해


@Around("@annontation(LoginCheck) && @annontation(loginCheck)")

해당 @Around 표현식은 Pointcut 조건과 바인딩을 동시에 수행한다.

먼저

@annotation(LoginCheck)
@LoginCheck 어노테이션이 선언된 메서드만 AOP 적용 대상으로 제한한다 (필터링).

그리고
@annotation(loginCheck)
해당 메서드에 선언된 @LoginCheck 어노테이션 인스턴스를
loginCheck라는 이름으로 바인딩한다.

이를 통해 AOP 메서드 내부에서

loginCheck.userType()

와 같이 어노테이션에 정의된 값을 직접 사용 가능하다
그래서

@annotation(LoginCheck) && @annotation(loginCheck) 구조를 사용했다.

Pointcut는 조건 대상을 고르고,
바인딩은 어노테이션 내부 값을 사용하기 위해 존재한다
@annontation의 역할이 달라 이해할때 헷갈렸다.

proceedingJoinPoint

AOP가 가로챈 실제 메서드 호출 정보를 담고있는 객체다.
실행할지 말지 어떻게 실행할지 결정한다

proceedingJoinPoint.getArgs()
실제 컨트롤러 메서드에 전달될 파라미터 배열을 반환한다.

public ResponseEntity<?> changePw(Long userId,..)

위와 같은 메서드를 가로챘다면
getArgs()는 해당 메서드의 인자들을 순서대로 담은 Object[] 를 제공한다

초기 구현에서는 index = 0으로 사용자 식별자를 주입했기 때문에 @LoginCheck가 적용된 메서드는 첫 번째 파라미터로 userId를 받아야 한다.
다만 이 방식은 메서드 시그니처에 제약을 주므로
이후 파라미터 어노테이션 기반 방식으로 개선할 여지가 있다.

@Aspect
@Component
public class LoginCheckAspect {

	@Around("@annotation(com.linking.backend.linking_backend.aop.LoginCheck) && @annotation(loginCheck)")
    public Object checkLogin(ProceedingJoinPoint proceedingJoinPoint, LoginCheck loginCheck)
    
    HttpSession session = (HttpSession)((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
    
    String userId = "";
    int index = 0;
    
    switch(loginCheck.type().toString()) {
    
    	case "USER" :
        	userId = SessionUtil.getLoginMemberId(session);
            break;
        
        case "ADMIN" :
        	userId = SesstionUil.getLoginAdminId(session);
            break;
    
    }
    if( userId == null) {
          throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "NO_LOGIN") {};
    
    }
    
    
    Object[] modifiedArgs = proceedingJoinPoint.getArgs();
    if(proceedingJoinPoint.getArgs()!=null)
        modifiedArgs[index] = userId;
    return proceedingJoinPoint.proceed(modifiedArgs);
    
}

문제점
1.index를 하드 코딩으로 잡았다.
-> 모든 컨트롤러마다 첫번째 인자값을 userId로 받아야됨
수정 방안 :

메서드에 aop 적용

설계한 @LoginCheck 어노테이션은
로그인 검증이 필요한 메서드에 직접 선언해 사용할 수 있다.

@PatchMapping("password")
@LoginCheck(type = LoginCheck.UserType.USER)
public ~~~~

https://devuna.tistory.com/53
https://chb2005.tistory.com/174
https://deveric.tistory.com/67
https://mynameiskgw.tistory.com/24

1개의 댓글

comment-user-thumbnail
2026년 1월 10일

한주동안 엄청 많은 걸 하신 것 같아요! 공부도 엄청 많이 하시구..2주차 개발 코스도 파이팅 해주세요~!!

답글 달기