[회고록] 캡스톤(졸작) 결산

2AST_\·2023년 6월 24일
0

회고록

목록 보기
2/2
post-thumbnail

캡스톤은 교내의 졸업작품 활동이다. 졸업논문 대체이기 때문에 매우 중요한 활동이다. 이번에 캡스톤에서 백엔드 파트와 팀장을 맡으면서 배운 것들과 아쉬운 점들을 써내볼까 한다.
(원래는 상반기 결산과 같이 요약하려고 했지만 분량 조절에 실패했다)

1. Express를 사용하다

처음에는 나는 스프링을 사용하기를 원했다. 스프링을 공부하고 싶었고 진로를 그 쪽으로 생각하니깐. 하지만 당시에는 나 혼자 백엔드를 하는 것이 아니였다. 같이 하는 팀원은 Express를 쓰기를 원했다. 해당 팀원이 스프링을 다루지 않는 것도 있고 생산성 측면에서도 Express가 뛰어나다고 설득했다.

나 또한 내 개인적인 공부보다는 생산성이 중요하다고 생각하여 이번에는 Express를 통해서 서버를 구축하게 되었다. 결과는 전체적인 서버 구축 과정을 경험해 전체적인 시야가 넓어진 것 같다.

2. Type 좀...

개발하면서 Typescript를 왜 쓰는지 알 것 같았다... 나도 알고 싶지 않았다. 중간에 타입스크립트를 천추의 한이 된다. Type이 맞지 않아 생기는 오류에 대해서 안일하게 생각했다. 에러 3할 정도가 Type이 맞지 않아서 생겼다. 나중에 Javascript 프로젝트를 진행하게 된다면 Typescript를 꼭꼭 적용할 것이다.

3. 3-Tier 아키텍처


이번 Express 에서는 3-Tier 아키텍처를 적용했다. 여러 블로그들을 참고하여 Express에서 아래와 같이 할당해주었다.

  • Route: URL과 비즈니스 로직 매핑
  • Controller: Model을 사용하여 비즈니스 로직 처리 담당
  • Model: 데이터 핸들링

수정
아키텍처 강의를 통해서 내가 설계한 아키텍처는 3-Tier가 아니라 Layered Architecture라는 것을 알게 되었다.

Route 예시

router.post("/sign-up", userController.userSignUp);

Controller 예시

exports.userSignUp = async function (req, res) {
    const { nickname, password, email, picture } = req.body;
    
    if(!validEmailCheck(email)) {
        res.status(400).send({
            success: false,
            message: "이메일 형식이 올바르지 않습니다."
        });
        return;
    }

    
    const hashedPassword = await Encryption(password);
    const USER_ROLE = 1;
    User.create(nickname, hashedPassword, email, picture, USER_ROLE, (err, user) => {
        if (err) {
            res.status(400).send({
                success: false,
                message: err.code,
            });
            return;
        }
        res.status(201).json({
            success: true,
            message: `Sign Up for  ${email}`,
            result: {
                userId: user.insertId
            }
        });
    });
}

Model 예시

User.create = function(nickname, password, email, picture, userRoleId, result){
    const INSERT_QUERY = `
        INSERT INTO ${table} (NICKNAME, PASSWORD, EMAIL, PICTURE, USER_ROLE_ID) 
        VALUES('${nickname}', '${password}', '${email}', '${picture}', ${userRoleId});
    `;
    mysql.query(INSERT_QUERY, (err, res)=>{
        if (err) {
            result(err, null);
        } else {
            result(null, res);
        }
    });
}

4. mysql에서 mysql2로

DB는 MySQL을 채택했는데, Javascript에서 Mysql DB와 연결하기 위해서 많은 예제가 있던 mysql 모듈을 사용했다. 그렇기 때문에 위와 같이 Model에서 콜백으로 쿼리를 처리했다.

하지만 프로젝트를 진행해서 트랜잭션을 처리할 상황이 발생했다. 문제는 기존의 mysql 모듈로는 트랜잭션 처리를 하지 못한다는 것. 그렇기 때문에 도중에 mysql2로 모듈을 교체하였다.

아래는 mysql2 교체한 뒤의 Product의 Controller와 Model 일부 코드이다. Product가 생성되는 동시에 해시태그들과 서브섬네일들이 생성 혹은 갱신되는 코드이다.

Controller

exports.create = async (req, res) => {
    console.info("Create Posts");
    const conn = await GetConnection();
    let pictureIdList = [];

    const {title, content, thumbnail, price, subthumbnails, hashtags, description} = req.body;
    try {
      	// 트랜잭션 시작
        await conn.beginTransaction();
   
        const productId = await Products.create(conn, req.user.id, title, content, thumbnail, price, description);
        console.info("product Create");

        // 서브 섬네일 로직
        for(const imageUrl of subthumbnails) {
            const pictureId = await Picture.createWithConn(conn, imageUrl, "PRODUCT");
            pictureIdList.push(pictureId);
        }

        // 서브섬네일 테이블 매핑
        pictureIdList.forEach((pictureId) => {
            Subthumbnail.create(conn, productId, pictureId);
        });
        console.info("subthumbnail create");

        // 해시태그 삽입
        if(hashtags) {
            for (const hashtagTitle of hashtags) {
                console.log(hashtagTitle);
                const findHashtag = await Hashtag.findByTitle(conn, hashtagTitle);
                if(!findHashtag) { //해시태그가 없으면
                    const hashtagId = await Hashtag.create(conn, hashtagTitle);
                    ProductHashtag.create(conn, productId, hashtagId);
                } else { //해시태그가 있으면
                    ProductHashtag.create(conn, productId, findHashtag.id);
                }
            }
        }
        

        await conn.commit();
        sendResult(res, "포스트 생성 완료", productId);
        return;
    } catch(err) {
        conn.rollback();
        console.error(err);
      // 그냥 다 500으로 처리할 껄 그랬다.
        if (err instanceof MysqlError) {
            sendError(res, err.message, 500);
        } else {
            sendError(res, err.message, 400);
        }
    } finally {
        ReleaseConnection(conn);
    }
}

Model

Products.create = async (conn, author_id, title, content, thumbnail, price, description) => {
    try {
        const INSERT_QUERY = `
            insert into ${TABLE} (author_id, title, content, thumbnail, price, description)
            values (?, ?, ?, ?, ?, ?);
        `
        const [res] = await conn.execute(INSERT_QUERY, [author_id, title, content, thumbnail, price, description]);
        return res.insertId;
    } catch(err) {
        console.error(err);
        throw new MysqlError("MYSQL ERROR");
    }
};

하지만 아직 아쉬운 부분들이 존재했다. 일반적인 스트링으로 쿼리문을 작성하고 실행하기 때문에 작은 실수들을 발견하기 어려웠다. 이 때 Object-Oriented Software Engineering에서 소개한 DB Wrapper를 만들지 않아 지나치게 많은 코드를 수정해 회사가 망한 사례가 스쳐갔다.

5. Test

기존의 테스트는 모두 Postman을 사용해서 확인하였다. 하지만 Postman을 쓰는 것보다 테스트 자동화되게 만드는 것이 더욱 좋다는 것을 깨달았다. 반복되는 일을 자동화하고 테스트가 가능한 코드를 만들려고 노력하다보니 모듈화가 되는 것을 실감했다.

지금까지 개발 시간이 아까워서 Test 자동화를 하지 않은 것에 대해 반성한다. 선수적으로 테스트를 설정, 자동화는 오히려 테스트 시간과 디버깅 시간을 줄일 수 있었다.

Unit 테스트는 적용하지 못했지만 Postman에 적용한 테스트처럼 인수 테스트를 할 수 있게 적용하였다. 다음에는 테스트에 대해 조금 더 공부해봐야겠다. 사용한 모듈은 supertestJest이다.

Test 예시

test("Get Posts LIKES", (done) => {
  request(app)
      .post(`/api/post/list`)
      .query({type:'like'})
      .send({
      "startTime": "2022-02-01T01:01:01",
      "endTime": "2024-02-01T01:01:01", 
      "offset": 0,
      "limit": 10,
      "keyword": "title"
    }).expect(200)
    .end((err, res) => {
      if(err) throw err;
      expect(res.body.result.length).toBe(2);
      expect(res.body.result[0].id).toBe(SECOND_POST_ID);
      console.log(res.body);
      done();
  })
});

6. 배포 자동화

저번 Solution Challenage에서 배포 자동화에 대한 필요성을 절실히 느꼈다. 그렇기 때문에 이번에는 배포 자동화를 Github ActionCode Deploy를 통해서 달성하였다.

일일이 EC2 인스턴스에 접속해서 프론트 파일 빌드와 pm2를 통해 서버를 실행시킬 필요가 없어졌다. 이제는 Master 브랜치로 Push 혹은 Pull Request를 보내면 서버가 자동으로 갱신된다!

7. AWS

AWS의 여러 기술들을 써볼 수 있었던 기회가 되었다. EC2 / RDS / S3 / Route 53 / Codedeploy 등등을 써보게 되었다. 이와 같은 AWS 기술들을 사용하여 달성한 것들은 아래와 같다.

  • 서버 배포
  • 인스턴스와 독립적인 DB
  • https 연결
  • 배포 자동화

8. Collaboration

그리고 마지막으로 협업에 관련해서 말해볼까 한다. 우리 팀은 우여곡절이 많았다. 6명으로 시작해서 3명이 개인사정으로 빠지게 되었다. 그렇게 해서 나 혼자 백엔드를 진행하게 되다 보니 코드를 짜는 것이 비교적 편했지만 스파게티 코드들이 우후죽순처럼 늘어간 것 같다.

그리고 나 포함 나머지 3명은 고등학교 때부터 친구였던 사이임에도 불구하고 소통이 매우 어려웠다. 실제로 서로 이해하는 것이 달라 프로젝트가 이상한 방향으로 가기도 했다.

해당 프로젝트에서 소프트웨어 엔지니어링 수업에서 배운 Common Understanding이 중요하다는 것을 깨달았다. 각자 맡은 파트만을 진행하는 것이 아니라 같이 도메인 모델링을 진행해 Common Understanding을 쌓아갔었으면 더욱 수월하게 진행할 수 있었을 것이다.

이번 캡스톤 프로젝트를 통해 각자의 역량보다는 협업 능력이 대두된다는 것을 깨닫게 되었던 귀중한 경험이 되었다.

9. 마무리

일부 교수님들에게만 심사받는 최종 발표에서는 부정적인 평가가 지배적이였다. 어떤 교수님은 아이디어가 특출나지 않아 거의 클론코딩이라는 평가를 받을 정도로... 우리는 거의 좌절한 상태였다.

마지막 일정은 캡스톤에 참가한 팀 모두 교내에서 자신들의 작품을 전시하는 것이다. 우리도 물론 작품을 전시했으며, 별 기대는 하지 않았지만 지금까지 만든 것이 아까워서 최대한 교수님들 및 외부인들에게 열심히 우리 프로젝트를 어필했다.

그리고... 장려상을 탔다! 최종 발표 결과가 절망적이라 수상은 포기한 상태였는데 운이 좋게, 팀원들 덕분에, 우리를 너그럽게 생각해주신 분들 덕분에 뜻밖의 상을 수상하게 되었다. 감정이 격해져서 시상대에서 나불나불 거린 것이 기억에 남는다.

이번 캡스톤은 여러모로 배울 점도 아쉬운 점도 많았던 과정이었던 것 같다. 이제 배운 점은 간직하고 아쉬운 점을 보완하는 단계를 거쳐가야 겠다.

Github: https://github.com/kookmin-sw/capstone-2023-06
시연연상: https://www.youtube.com/watch?v=3H6TQfn8TNo

0개의 댓글