본문 바로가기
Study/DevOps

[DevOps] Github Actions로 Workflow 자동화하기

by DevJaewoo 2022. 8. 31.
반응형

Github Actions LOGO

Intro

Github Actions는 Github의 push, pull 등의 이벤트 발생 시 해당 이벤트에 대해 미리 정해둔 동작자동으로 수행하는 도구다.

이를 통해 pull 하기 전 자동으로 테스트를 수행하여 Fail 시 PR을 취소시키고 Slack을 통해 알림을 준다거나, 배포용 브랜치에 push 이벤트가 발생할 경우 AWS에 자동으로 Deploy 하는 등의 CI/CD 구성이 가능하다.

 

이번 시간엔 간단한 예제를 통해 Github Actions를 사용하는 방법에 대해 알아보자.


테스트 프로젝트 생성

Github Actions를 사용하기에 앞서 테스트용 프로젝트를 만들어주자.

아래와 같이 4개의 Dependency만 추가했다.

새 프로젝트 생성

 

그리고 간단한 웹 어플리케이션을 작성한다.

Github 링크에 소스코드를 올려뒀으니 src 폴더를 복사해도 된다.

 

GitHub - DevJaewoo/github-action-test

Contribute to DevJaewoo/github-action-test development by creating an account on GitHub.

github.com

 

User.java

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Enumerated(EnumType.STRING)
    private UserAuthority authority;

    public User(String name, UserAuthority authority) {
        this.name = name;
        this.authority = authority;
    }

    public User(String name) {
        this(name, UserAuthority.ROLE_CLIENT);
    }
}

 

UserAuthority.java

public enum UserAuthority {
    ROLE_ADMIN,
    ROLE_CLIENT,
}

 

UserRepository.java

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByName(String name);
}

 

UserService.java

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    @Transactional
    public Long join(User user) {
        if(user.getName().isEmpty()) {
            throw new IllegalArgumentException("유효한 회원명이 아닙니다.");
        }

        if(userRepository.findByName(user.getName()).isPresent()) {
            throw new IllegalArgumentException("이미 존재하는 회원입니다.");
        }

        userRepository.save(user);

        return user.getId();
    }
}

 

UserRepository.java

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/join")
    public String join(@RequestParam String name) {
        User user = new User(name);
        Long id = userService.join(user);
        return "Success! ID: " + id;
    }
}

 

application.yml

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/test
    driver-class-name: org.postgresql.Driver
    username: postgres
    password: postgres

  jpa:
    hibernate:
      ddl-auto: create

 

빌드 후 정상적으로 실행되는지 확인해보자.

API 테스트


테스트 케이스 작성

Github Actions가 자동으로 실행해줄 간단한 테스트 케이스들을 작성한다.

UserTest.java

class UserTest {

    @Test
    @DisplayName("User 기본 권한이 Client로 설정되었는지")
    public void User_Constructor_DefaultAuthorityTest() {
        User user = new User("user");
        Assertions.assertThat(user.getAuthority()).isEqualTo(UserAuthority.ROLE_CLIENT);
    }
}

 

UserServiceTest.java

@SpringBootTest
@Transactional
class UserServiceTest {

    @Autowired UserRepository userRepository;
    @Autowired UserService userService;

    @Test
    @DisplayName("회원가입 정상적")
    public void UserService_join_success() {
        //given
        User user = new User("user");
        
        //when
        Long id = userService.join(user);

        //then
        Assertions.assertThat(id).isGreaterThan(0);
    }

    @Test
    @DisplayName("회원가입 사용자명 공백 실패")
    public void UserService_join_emptyUsername() {
        //given
        User user = new User("");

        //when
        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> userService.join(user));

        //then
        Assertions.assertThat(e.getMessage()).isEqualTo("유효한 회원명이 아닙니다.");
    }

    @Test
    @DisplayName("회원가입 사용자명 공백 실패")
    public void UserService_join_duplicateUsername() {
        //given
        User user = new User("user");

        //when
        userService.join(user);
        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> userService.join(user));

        //then
        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

Github Action 만들기

이제 main 브랜치에 push, pull 할 때 코드를 자동으로 테스트하는 Action을 추가해보자.

아래 사진처럼 Actions 탭에 들어가서 Github에서 바로 생성할 수도 있지만, 연습 삼아 로컬에서 생성하고 push 해보자.

Github Actions 템플릿

 

Github Action은 /github/workflows 폴더 안의 파일을 실행한다.

ci-test-main.yml 파일을 /github/workflows 폴더에 만들어주자. 파일명은 다르게 해도 괜찮다.

코드에 대한 설명은 주석으로 달아놨다.

 

ci-test-main.yml

# ci-test-main.yml

# workflow의 이름을 정의한다.
name: 'ci-test-main'

# workflow가 언제 동작할지 정의한다.
# 이 workflow의 경우 main branch에 push 또는 pull_request 이벤트가 발생할 경우 동작한다.
on:
  push:
    branches:
      - 'main'
  pull_request:
    branches:
      - 'main'

# job은 사용자가 정한 플랫폼을 통해 step이라는 일련의 과정을 실행할 수 있다.
# 여러 개의 job을 사용할 수도 있고, job끼리 정보 교환도 가능하다.
jobs:
  # test라는 job을 정의한다.
  test:
    # job의 이름을 정의한다.
    name: Build and test project

    # job이 실행될 환경을 정의한다.
    runs-on: ubuntu-latest

    # job 실행 중 필요한 서비스들을 정의한다.
    # Docker Container로 설정할 수 있다.
    # 이 프로젝트는 PostgreSQL을 사용했기 때문에 postgres 환경으로 구성했다.
    services:
      postgres:
        image: postgres:latest
        env:
          # application.yml에서 DB를 test로 지정했었다.
          POSTGRES_DB: test
          POSTGRES_PASSWORD: postgres
          POSTGRES_USER: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    # step에선 쉘 스크립트나 이미 만들어진 action 등을 사용할 수 있다.
    steps:
      # Github Acitons는 프로젝트를 CI 서버로 내려받아 특정 브랜치로 checkout하여 실행한다고 한다.
      # 원래는 쉘 스크립트로 CI 서버, 코드 저장소 간 인증, 절차 등을 신경써서 일일이 작성해줘야 하지만,
      # 다른 사람이 만들어둔 action이 이미 있기 때문에 코드 한 줄로 갖다 쓸 수 있다.
      - uses: actions/checkout@v3

      # 이 프로젝트는 JAVA 환경에서 동작하기 때문에 CI 서버 또한 JAVA 환경을 구성해줘야 한다.
      # checkout과 마찬가지로 다른 사람이 만들어 둔 action이 있기 때문에 갖다 쓰면 된다.
      - name: Setup JAVA
        uses: actions/setup-java@v3
        with:
          # 프로젝트에서 corretto-17 JDK를 사용했기 때문에 그에 맞게 구성해줬다.
          java-version: '17'
          distribution: 'corretto'

      # gradle build 진행 시 실행 가능한 파일이 아니라는 에러를 띄우며 fail이 나기 때문에 실행 권한을 줘야 한다.
      - name: Add executable permission to gradlew
        run: chmod +x ./gradlew

      # gradle build 작업 안에 test 작업도 기본적으로 들어가있기 때문에 build만 해주면 된다.
      # gradle build 작업도 다른 사람이 만들어 둔 action을 사용했다.
      - name: Gradle build
        uses: gradle/gradle-build-action@v2
        with:
          arguments: build

 

아래의 링크에서 각 step에 대한 사용법을 확인할 수 있다.

 

코드를 origin/main 브랜치에 push 하면 아래와 같이 workflow가 자동으로 실행되는 것을 볼 수 있다.

원래 #1부터 시작해야 되는데 여러 가지 오류들을 해결하다 보니 #7번째에 성공했다.

CI 성공


테스트 Fail 되는 경우 테스트

Branch를 하나 새로 만들어서 Fail이 날 수밖에 없는 테스트 케이스를 추가하고, main에 Pull Request 시 Test가 정상적으로 Fail 되는지 확인해보자.

 

test/failure라는 브랜치를 새로 만들고 아래의 테스트 케이스들을 추가하자.

 

UserTest.java

class UserTest {

    ...

    @Test
    @DisplayName("단위테스트 실패 테스트케이스")
    public void failure() {
        Assertions.assertThat(1 == 2).isTrue();
    }
}

 

UserServiceTest.java

@SpringBootTest
@Transactional
class UserServiceTest {

    ...

    @Test
    @DisplayName("통합테스트 실패 테스트케이스")
    public void failure() {
        //given
        User user = new User("user");

        //when
        Long id = userService.join(user);

        //then
        Assertions.assertThat(id).isEqualTo(0);
    }
}

 

Github에 Push 하고 main으로 Pull Request를 보내면, 아래와 같이 테스트가 자동으로 진행되고 Fail 된 것을 볼 수 있다.

CI 실패

Details로 들어가 보면 아래와 같이 이전에 추가한 2개의 테스트 케이스가 Fail 된 것을 볼 수 있다.

CI 실패 로그


결론

이번 시간엔 Github Actions를 통해 테스트를 자동화하는 방법에 대해 알아봤다.

지금은 간단하게 테스트만 자동화해봤지만, 어떻게 사용하느냐에 따라 활용할 수 있는 방법이 무궁무진할 것 같다.

또한 작업을 자동화해주는 만큼 개발 시간을 많이 단축시켜 줄 것 같다.


참고자료

 

 

 

 

반응형