개요

Spring, 또는 SpringBoot 둘 중 하나를 택해 웹 서비스를 개발하는 수업의 과제물로 제출한 프로젝트이다.

이전에 기능을 구현할 때에는 과제 마감일에 쫓겨 가독성이 좋지 못한 코드를 작성했었다. 금번 리팩토링에서는 이러한 문제를 해결하고 코드의 품질을 높이는 방법에 대해 팀원들과 함께 공부해보았다. 회의는 특별한 이변이 없는 한 매주 진행하였다.
총 3명의 팀원이 함께하였으며, 로버트 C. 마틴의 저서 <클린 코드(애자일 소프트웨어 장인 정신)>를 읽고
각자 학습한 바를 정리하고 이를 바탕으로 테스트 작성과 리팩토링을 하였다.
여기에 팀장으로서 전반적인 개발 과정을 검토하고, 코드 리뷰와 회의를 주도하였다.

2편은 테스트 작성과 리팩토링을 다룬다. 🔗1편 링크는 이곳

 

✨Git 링크

https://github.com/jinju9553/SOMusic-SpringBoot

 

수행 기간

2022.01.08 ~ 2023.05.10

 

기술 스택

Language: Java 1.8
Framework : SpringBoot, Spirng Data JPA
View: JSP, ThymeLeaf
DB: Oracle Database
Test: JUnit5, Mockito

 

수행 과정

  • 시작 전에 점검한 것들
    • 프로그램 구조도를 수정해야 하는지 고민했으나, 리팩토링 과정에 설계와 기능 수정은 포함되지 않는다.
    • 15라인이 넘어가는 함수 분리, 들여쓰기 수정, 불필요한 주석 삭제, 메소드 파라미터의 개수 줄이기 등 코드의 가독성을 높일 수 있는 방안을 검토하였다.
    • <클린 코드> (로버트 C. 마틴), <리팩토링 1판> (마틴 파울러), <객체지향의 사실과 오해> (조영호) 중에서 언어에 구애받지 않고 여러 코드 리팩토링에 가장 적용하기 쉬운 첫 번째 서적을 선택하였다.
  • 1주차
    • 각 클래스 별로 어느 메소드를 어떻게 수정해야 할지, 리팩토링할 목록을 정리하였다.
  • 2주차
    • <클린 코드>를 읽고 중요한 내용들을 문서화시켜 정리하였다.
  • 3주차~8주차
    • 각 클래스 별로 테스트 코드를 작성하였다. 본래 TDD 개발방식에 따르면 기능 개발에 앞서 테스트를 미리 작성하는 것이 맞지만, 본 프로젝트 는 스프링 프레임워크를 배우는 강의의 과제물로써 개발한 것이고, 일전에 별도의 테스트 작성법을 학습한 적이 없기에 부득이하게 기능 개발 이후에 테스트를 작성하게 되었다. 코드리뷰는 2~3주에 한 번씩 시행한다.
    • 테스트 작성 과정 중에 맞닥뜨린 오류는 다음 항목에 정리한다.
  • 9주차~15주차
    • 완성된 테스트 내용을 바탕으로 코드를 리팩토링한다. 상기한 내용처럼 길이가 긴 함수를 최대한 분리하고 조건문을 캡슐화하여 가독성을 향상시키고 의미를 파악하기 쉽도록 하였다. 또, 강의 수강 당시 학습용으로 작성해두었던 주석들을 모두 삭제하였다. 마찬가지로 코드리뷰는 2~3주에 한 번씩 시행한다.
    • 리팩토링 과정 중 맞닥뜨린 오류는 다음 항목에 정리한다.
  • 16주차
    • 코드 리뷰와 회고를 작성한다.

 

문제 해결

  • Controller 테스트 코드의 404번 오류
    • 참고한 링크: https://sanghaklee.tistory.com/61
    • REST API에서는 주로 경로가 존재하지 않거나 요청한 자원이 존재하지 않을 떄 404번 상태 코드를 응답한다.
    • 본 테스트에서는 테스트 코드와 연결된 컨트롤러 명에 오탈자가 존재해서 404번 응답을 받게 되었다.
    • 따라서 Controller 테스트 클래스의 @WebMvcTest 어노테이션 안의 컨트롤러 명칭의 오탈자를 수정해주었더니 해결되었다.
    • 하지만 이어서 아래 오류를 받게 된다.
    • Failed to load ApplicationContext 에러
      • 테스트 코드에서 AccountFormValidator의 선언 및 Bean 등록이 빠져있어서 발생한 오류이다.
      • 협력 객체들을 테스트 코드에서도 모두 선언하고 MockBean으로 등록해주어야 테스트가 정상적으로 구동한다.
      • 모든 협력 객체를 등록하고 API로부터 200 OK 응답을 받은 것을 확인하였다.
  • Enum의 도입
    • 본 웹 서비스의 상품 주문 기능에서는 shippingCost(배송비) 라는 데이터를 활용한다. 이 배송비는 총 3가지 존재하고 사용자가 원하는 배송 방식을 선택하여 그에 따라 각기 다른 배송비가 최종 주문 금액에 합산된다.
    • 이 배송비는 ModelAttribute로 설정하여 클라이언트 단에 전송되고, 리턴 타입은 Object[]이다. 그런데 이전에는 이 배송비와 배송비 표시 텍스트("준등기", "택배" 등의 글씨) 등을 ModelAttribute 함수 내에 배열로 생성하여 리턴하였다.
    • 그러한 방식을 사용하다보니 다른 컨트롤러에서 똑같이 배송비를 클라이언트에 전송하는 작업을 수행하려면 코드를 복사 + 붙여넣기 해야 했고, 불필요한 코드 반복이 나타났다.
    • 이 문제를 해결하기 위해 ShippingCost라는 Enum을 정의하여 그 안에 배송비와 텍스트, 고유 번호 등을 선언하고 상수처럼 값을 꺼내 쓰는 형태로 리팩토링 하였다. 모든 값을 리스트로 반환하는 비즈니스 메소드도 함께 정의하였다.
    • 이렇게 하면 배송비 데이터를 필요로하는 컨트롤러에서 ShippingCost.getCostList() 한 줄로도 우리가 원하는 데이터를 전송할 수 있다. 일일이 데이터를 타이핑하거나 붙여넣기 하다가 오탈자가 생길 염려를 하지 않아도 된다.
  • Optional 클래스의 활용
    • 참고한 링크: https://mangkyu.tistory.com/70
    • Stream을 통해 데이터를 처리할 때 타입 오류를 맞닥뜨리고서 Optional 타입이 무엇인지 알게 되었다.
    • Optional 타입은 쉽게 말하면 null이 발생할 수 있는 값을 감싸는 Wrapper 클래스이다. 불필요한 null 처리를 줄일 수 있다.
    • 이때, StreamfindAny()로 발견한 값들도 Optional 타입으로 반환된다.
    • 따라서 Optional<T>.get()으로 안에 들어있는 내용물을 별도로 꺼내주어야 한다.
  • BindingResultHttpSession 객체를 테스트에 활용하는 방법
    • 이러한 객체들은 단순히 인스턴스를 생성하는 방식으로는 사용할 수 없다. 대신 Mockito.mock()으로 Mock 객체(가짜 객체)를 만들어주면 테스트에 활용할 수 있다.
    • MockHttpSession 객체는 다음과 같은 방식으로 생성한다.
      Login userSession;
      
      MockHttpSession session;
      
      @BeforeEach() //다른 테스트 메소드보다 가장 먼저 실행된다.
      public void setUp() {
      	Account account = new Account();
      	account.setUserId(USER_ID);
          
      	userSession = new Login(account);
          
      	session = new MockHttpSession();
      	session.setAttribute("userSession", userSession);
      }
  • 테스트 함수의 인자로 captor.capture() 와 함께 raw String을 줄 때
    • 참고한 링크: https://github.com/HomoEfficio/dev-tips/blob/master/Mockito Stub 작성 시 주의 사항.md
    • Mockito.verify(accountDao).updatePassword(captor.capture(), NEW_PASSWORD); 에서 발생한 문제이다.
    • 이 경우, 함수의 모든 인자를 Matcher를 통해 전달해주어야 하기 때문에 raw String을 인자로 쓸 수 없다.
    • 따라서 String을 ArgumentMatchers.eq()로 감싸준 뒤 함수의 인자로 넘겨준다.
    • ArgumentMatcherArgumentCaptor를 쓸 때는 그 인자로 반드시 Matcher를 사용한 값을 주어야 한다. any() 메소드 또한 Matcher 메소드의 일종이다.
  • Cannot resolve symbol 'Assert’ 오류
    • 참고한 링크: https://aonee.tistory.com/2
    • 코드에서 사용된 버전의 jUnit 라이브러리가 프로젝트 내에 존재하지 않아서 발생하는 오류이다. 나와는 다른 개발 IDE를 사용하는 팀원이 작성한 테스트 코드를 pull해온 뒤에 이 오류가 발생하였다.
    • Maven dependency(pom.xml)에 다음과 같이 jUnit 4.12버전의 JAR을 import하는 부분을 추가해주면 오류를 없앨 수 있다.
      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
      </dependency>

 

소감

  • 학교 과제와 무관하게 진행된 스터디 모임이었다. 그 덕분에 마감일에 쫓기지 않고 공부를 할 수 있었다.
  • JUnit을 이용해 테스트 함수를 작성해본 것은 이번이 두 번째였다. 그러나 JUnit 테스트를 처음 활용해보았을 때는 MVC 구조의 웹 어플리케이션이 아닌, 일반적인 자바 객체의 유효성을 검증하는 등의 기능만 사용해보았던 탓에 웹 어플리케이션의 테스트 코드는 어떻게 작성해야 하는지 헤메기도 하였다.
  • 다행히도 인터넷에 SpringBoot 기반으로 테스트 코드를 작성하는 방법을 잘 정리해놓은 자료들이 제법 있어 도움이 많이 되었다. 검색의 힘이 가장 컸던 것 같다. 블로그 게시글들을 보면서 차츰 라이브러리 내의 메소드 사용법에 익숙해지도록 하였다.
  • 그 다음에는 given - when - then 패턴을 핵심으로 삼아 테스트코드를 작성하였다. 이러한 뼈대가 있다고 생각하니 테스트 작성에 막연함이 덜한 느낌이었다. (참고한 블로그 링크는 이곳)
  • 위 패턴에 따라 테스트 코드를 작성하고, 작성 중 모호한 부분이 있다면 클린코드 17장: '냄새와 휴리스틱'에 수록된 원칙들을 따랐다. 가능한 클래스 내의 모든 메소드를 테스트해보려 하였다. 주로 참고한 원칙들은 다음과 같다.
    • T1: 불충분한 테스트는 독이다.
    • T3: 사소한 테스트도 건너뛰지 마라.
    • T4: 무시한 테스트는 모호함을 뜻한다.
    • T5: 경계 조건을 각별히 신경 써서 테스트하라.
    • T7: 실패 패턴을 살펴라.
  • 다만 `T2: 커버리지 도구를 사용하라` 의 원칙은 고려하지 못한 것이 아쉽다. 테스트 메소드 작성과 리팩토링에 급급해 커버리지를 미처 신경쓰지 못하였다. 회고를 작성한 후에도 시간을 내어 테스트 커버리지를 확인해보고 싶다.
  • 테스트 작성을 마치고, 리팩토링 과정에서는 상기한 내용처럼 코드의 가독성을 높이는 데에 집중했다. 메소드명을 통해서 의미를 드러내고, 메소드가 수행하는 일을 문장으로서 나타내는 것이 참 중요하다고 느꼈다. 팀원의 코드 중 몇몇 부분은 잘 이해하지 못하기도 했었는데, 그 또한 메소드명을 수정하고 몇몇 코드를 캡슐화한 덕분에 그것이 의미하는 바를 쉽게 이해할 수 있게 되었다.
  • <클린 코드> 내에서도 강조했던 내용이지만, 결과적으로 프로그램의 코드 또한 사람이 읽는 문서의 일종이고, 그만큼 협업하는 사람들이 이해하기 쉽도록 작성하는 것이 중요하다는 것을 다시 느꼈다. 

개요

Spring, 또는 SpringBoot 둘 중 하나를 택해 웹 서비스를 개발하는 수업의 과제물로 제출한 프로젝트이다.
총 4명의 팀원이 함께 협업하였으며, 수업에서 배운 모든 분야의 지식을 두루 활용하기 위해
프론트/백의 구분을 두지 않고 전원이 풀스택으로 개발하였다.
여기에 팀장으로서 과제 산출물과 전반적인 개발 과정을 검토하고, 회의를 주도하였다.

1편은 기능 구현에 초점을 두어 작성하고, 2편은 테스트 작성과 리팩토링을 다룬다.

 

✨Git 링크

https://github.com/jinju9553/SOMusic-SpringBoot

 

수행 기간

2022.03.15 ~ 2022.06.23

 

기술 스택

  • Language: Java 1.8
  • Framework : SpringBoot, Spirng Data JPA
  • View: JSP, ThymeLeaf
  • DB: Oracle Database
  • Test: JUnit5, Mockito

Spring을 먼저 학습한 후 SpringBoot를 뒤늦게 배웠는데, 시간이 촉박하더라도 SpringBoot로 개발하는 것을 택했다.
JpaRepository 인터페이스를 사용하면 메소드 작명 규칙에 따라 쿼리가 자동으로 생성되고,
Lombok 의 어노테이션을 이용하면 GetterSetter 까지 자동으로 생성되는 등,
Spring보다 다양하고 편리한 기능들이 많이 정의되어 있었기에 한정된 기간 내에 원하는 기능을 모두 개발할 수 있을 거라 판단했다. 겸사겸사 문서를 읽고 라이브러리를 사용하는 요령도 길렀다.

 

✨기능

주요 기능 3가지 정의

 

✨수행 과정

  • 2주차
    • 주요 활동 주제 및 개발범위를 선정하고, 개발 업무를 분담하였다.
  • 3주차
    • 요구사항을 분석하고 세부 기능을 정의하였다.
    • 실제로 구현이 어떤 방식으로 이루어지는지 몰랐기 때문에 요구사항을 과도하게 세분화하는 불상사가 발생했다.
    • 결국 최종 보고서 제출 시에 세분화 된 몇몇 기능을 통합해서 도표를 작성하기도 했다.
  • 4주차
    • UI를 설계하고 Request 처리 흐름도를 간략하게 작성하였다.
    • 요청 흐름 작성은 Spring 기반 웹 프로그램에서 어떤 방식으로 요청이 발생하고 어떤 흐름으로 요청이 전달되는 지 정확하게 알아야만 작성할 수 있는 도표이다. 처음 MVC 구조를 학습할 때에는 쉽사리 구조가 파악되지 않아 교안을 반복해서 확인하기도 했다. 후에는 이 지식을 확실히 내것으로 만들기 위해서 빈 종이 위에 Controller, View, Service 등을 표시하고 그 사이에 오가는 요청을 도표로 그려보기도 하였다. 이 방식이 특히 프로그램의 흐름을 익히기에 적합한 것 같다.
  • 5주차
    • 데이터베이스 스키마(논리적, 물리적)를 설계하고 도메인 클래스를 작성하였다.
    • DB 스키마는 설계 단계에서부터 신중을 기해야 한다고 생각한다. 일전의 프로젝트에서는 각 테이블 별 연관 관계 특성 상 데이터가 꼬이는 바람에, 요구사항에 맞는 API를 설계하기 위해서 DB를 지우고 다시 설계해야만 하는 불상사가 발생한 적 있다.
    • 이 과정에서 특히나 도움이 되는 것은 요청 흐름과 데이터의 이동을 말로 풀어서 설명하는 것이다. DB 스키마는 현실 세계에 존재하는 문제 상황을 데이터로 모델링한 산출물이라고 생각한다. 이 산출물을 올바르게 내기 위해서는 요청 흐름과 요구사항을 정확하게 파악해야 하며, 데이터가 어디에서 어떻게 전달되어야 하는지를 사람의 언어로 잘 풀어내는 것이 요청 흐름과 프로그램 구조를 이해하는 데에 도움이 된다.
    • 이 때문에 팀원들과 온라인 화상 회의에서 몇 시간동안을 말로 설명하고 이해한 것을 DB 구조로 옮겨 적었던 기억이 난다.
  • 6주차
    • 설계한 데이터베이스 스키마를 토대로 ER 다이어그램을 작성한다.
    • 설계서 제출을 위해 개체관의 관계와 각 컬럼의 의미를 풀어서 설명하였다. 상기한 바와 같이 설계서도 결국 사람이 읽는 것이기 때문에 명료하고 이해하기 쉬운 언어로 내용을 설명하는 것이 중요하다고 생각한다. 팀 내의 어떤 사람이 읽어도 중의적으로 해석되지 않고, 한 가지 의미로만 뚜렷하게 설명되어야 하지 않을까 싶다.
  • 7주차
    • Controller 클래스, Service 클래스를 정의하고 Request 처리 흐름도를 최종적으로 작성하였다.
    • 요청 처리 흐름은 Use Case별로 구분하여 작성하였다. 마찬가지로 흐름도를 명료하게 작성하기 위해서라면 프로그램의 실행 구조에 대한 이해가 수반되어야 한다.
    • Controller에서 어떤 handler 메소드를 거쳐야 하는지, 그 메소드에서는 어떤 Service 메소드를 호출하는지, 그 Service 메소드는 또 어떤 DAO 메소드를 호출하고, DAO 메소드는 DB에 접근하여 어떻게 데이터를 처리하고 어떤 데이터(View 또는 Model)를 반환하는지 등등... 수업에서 제공한 예제 프로젝트와 예제의 설계도를 반복적으로 읽으며 분석하였다.
  • 9주차~10주차
    • 중간고사를 마치고 Handler 메소드의 이름을 정의하고, DAO 인터페이스 및 Controller 클래스를 다이어그램으로 정의하였다.
    • 슬슬 산출물이 쌓이는 단계이기도 하고, 설계를 토대로 실질적으로 기능 구현에 들어서는 시기였다. 어떤 클래스를 얼마나 정의하고, 그 안에는 무슨 메소드가 정의되어 있는지를 미리 설계했기 때문에 이를 토대로 개발을 시작하기가 비교적 수월했다.
  • 11주차
    • 전체 기능 중 먼저 구현해야 하는 우선순위를 정하고, 커밋메시지 양식을 통일하였다.
    • 220512 login fix와 같이 yyMMdd + 기능 설명(한글or영어) 으로 통일하였는데, 당시에는 흔히 통용되는 커밋 메시지 양식을 잘 몰라서(feat, fix, style 등등) 이와 같은 형식으로 결정했었다. 조금 더 검색해보고 결정했을걸 하는 아쉬움이 남는다.
  • 12~16주차
    • 본격적으로 기능을 구현해나갔다. 맞닥뜨린 오류와 해결 방법은 아래의 항목에 적는다.

 

✨문제 해결

  • 스프링부트에서 jsp 파일을 인식할 수 없었던 오류
    • application.properties 파일에 경로가 올바르게 설정되어 있는지 검토한다. thymeleaf로 정의된 View는 대개 classpath:templates 디렉토리 아래에 배치되지만, jsp 파일은 그렇지 않다. 본 프로젝트에서는 jsp 파일이 WEB-INF/jsp 디렉토리 아래에 위치하기 때문에 적절하게 파일 경로를 나타내는 prefix와 suffix를 수정해주었다.
  • SQL: 인덱스에서 누락된 IN 또는 OUT 매개변수
    • 질의문과 여기에 매핑되는 Object의 컬럼수가 맞지 않을 때 발생한다. 본 프로젝트에서는 JpaRepositoryspringframework 라이브러리에 정의된 @Repository 어노테이션으로 자동 생성되는 쿼리문 외에도 특정 기능 구현을 위해 JdbcTemplate 기반으로 직접 쿼리문을 작성할 필요가 있었다. 요청하는 데이터의 가짓수가 많을 수록 혼동할 확률이 높으니 쿼리문과 Object가 알맞에 매핑되었는지 검토할 필요가 있다.
  • ORA-00911: 문자가 부적합합니다
    • SQL문 중간에 세미콜론(;)이 포함되는 등, SQL문 자체에 문제가 있을 때 발생하는 오류이다. 오탈자가 없는지 점검하였다.
  • class [I cannot be cast to class [Ljava.lang.Object; ([I and [Ljava.lang.Object; are in module java.base of loader 'bootstrap')
    • ModelAttributeint[] 타입 객체를 전송할 때 발생하는 에러이다. ModelAttribute로는 String[] 또는 Object[] 타입만 전송할 수 있다.
    • ModelAttribute로는 primitive type을 보낼 수 없음에 유의해야 한다.
  • ORA-02296: 사용으로 설정 불가 - 널 값이 발견되었습니다.
    • DB에서 null값으로 생성된 컬럼의 디폴트를 not null로 설정할 때 발생하는 오류이다. String의 경우 공백문자 등을 이용해 초기화 시키고 값을 바꿔주어야 한다.
  • Neither BindingResult nor plain target object for bean name“000”available as request attribute
    • 직역하면 Request Attribute로 사용할 "000"이라는 Bean이 BindingResult나 plain target object로 전송되지 않았음.
    • 즉, View로 보내는 Request에 command 객체를 담지 않아서 발생하는 문제이다. 누락된 Attribute가 없는지 점검한다.
  • java.sql.SQLException: 부적합한 열 인덱스 오류
    • 쿼리문의 파라미터 개수와 거기에 삽입하는 값의 개수, 또는 위치가 달라서 발생하는 오류이다. 오탈자가 없는지 점검한다.

 

✨소감

  • SpringBoot를 이용해 두 번째로 개발해보는 웹 프로젝트인데, Spring의 DI나 IoC등의 원리를 학습한 것은 처음이다.
  • 아직 프레임워크의 동작에 익숙하지 않기 때문에, 데이터베이스와 연동된 기능을 구현하고, CRUD 기능이 온전히 작동하는 프로그램을 만드는 데에 초점을 두어 학습하였다. 
  • 그렇기 때문에 주로 프레임워크를 처음 사용한다는 점에서 비롯된 어려움이나 문법 오류 등이 발목을 잡았다.
  • 또, 과제 제출에 초점을 맞추어 학습하다보니 코드의 질이나, Spring의 작동 원리에 대해서는 크게 고려하지 못했던 점이 아쉽다.
  • 요청 처리 흐름을 파악하기 어렵거나, 프로그램의 구조를 이해하지 못했을 때에는 상기한 바와 같이 그림을 그려 이해하려고 했다.
  • 개발 시간이 넉넉하지 않은 탓에, 구현 중 맞닥뜨린 어려움을 정공법으로 해결하지 못하고 임시방편으로 기능만 돌아가도록 만든 것이 큰 아쉬움으로 남았다.
  • 그 탓에 코드가 난잡해지고 직관적으로 이해하기 어려울 뿐더러, 불필요한 코드 반복이 나타나기도 했다. 이 점은 다음 포스팅인 리팩토링 편에서 해결 방안을 다룰 것이다.

개요

게임개발동아리에서 약 3~4주간 Lua언어와 Solar 2D 사용법을 학습하였다.

점검 차원으로 그동안 학습한 내용들을 토대로 자신만의 미니게임을 만들어 보는 시간을 가졌다.

나는 쥬니버 플래시게임에서 흔히 볼 수 있는 날아다니는 장애물 피하기 + 떨어지는 아이템 수집 형식을 골랐다.

 

프로젝트 기간

2019.01.03~2019.01.19

 

✨플레이 영상

실패 영상으로 촬영하였다.

 

✨구현 내용 및 코드

좌우 화살표(◀, ▶) 버튼을 클릭하여 캐릭터를 움직인다.

벽을 맞고 튕겨다니는 가시열매에 맞으면 점수가 20점 깎이고,

캐릭터가 하늘에서 떨어지는 사과를 받으면 100점이 추가된다.

제한 시간(1분) 내에 1500점 이상을 달성하면 클리어.

가시열매와 사과가 충돌하면 사과는 사라져버리니 주의.

 


선언부

  • 이 게임을 구현하는 데 가장 중요한 역할을 하는 physics 라이브러리를 호출한다.
  • physics 물리엔진을 사용하는 객체들은 모두 시뮬레이션 구동 후 자동으로 상호작용한다.
  • 모든 physics 시뮬레이션을 실행하기 앞서, 반드시 physics.start() 함수를 호출해야 한다.
math.randomseed( os.time() )

local widget = require( "widget" )
local composer = require( "composer" )
local physics = require("physics")
physics.start()
physics.setGravity(0, 9.8*0.75) //중력가속도

local scene = composer.newScene()
local i, j, score = 0, 0, 0

-- GUI
local background
local gameUI = {} //게임에 필요한 UI 오브젝트를 배열에 담아서 사용

//가시열매가 맞고 튕겨나갈 수 있는 벽을 생성하는 함수
function createWalls() 
    local wallThickness = 10

    //Left wall
    local wall = display.newRect(0, display.contentHeight/2, wallThickness, display.contentHeight)
    physics.addBody(wall, "static", {friction=0, bounce = 1})
    //Top wall
    wall = display.newRect(display.contentWidth/2, 0, display.contentWidth, wallThickness)
    physics.addBody(wall, "static", {friction=1, bounce = 1})
    //Right wall
    wall = display.newRect(display.contentWidth - wallThickness + 10, display.contentHeight/2, wallThickness, display.contentHeight)
    physics.addBody(wall, "static", {friction=1, bounce = 1})
    //Bottom wall
    wall = display.newRect(display.contentWidth/2, display.contentHeight - wallThickness + 10, display.contentWidth, wallThickness)
    physics.addBody(wall, "static", {friction=1, bounce = 1})

    // bounce 값이 한 쪽 벽에서만 커지면 가속도가 붙는다 ==> 속도 무한대로 증가!
    // 따라서 bounce 값은 4면에서 모두 일정하게 유지
end

 

CreateApple() 함수

  • 임의의 위치에 사과를 생성하여 드롭한다.
  • 오브젝트 간의 충돌효과를 위해 physics.addBody를 적용한다.
  • 사과는 땅으로 가까워질 수록 투명해진다.
  • 사과에 collision이라는 EventListner를 추가해주었다. 이제 충돌을 인식할 수 있다.
function createApple() // 새로운 사과 생성

        local apple = display.newImageRect("Fruit/나며꾼사과.png", 70, 70)
        apple.x, apple.y = player.x + math.floor(math.random(-500, 500)), player.y/8 // 위치는 랜덤
        apple.alpha = 1

        physics.addBody(apple, "dynamic", {friction = 0, bounce = 1.25})
        apple:addEventListener("collision", apple)

        // 만약 사과가 플레이어와 충돌(collision)한다면? ==> 점수 획득

        apple.collision = function(self, event)
            if(event.other.type == "destructible") then
                self:removeSelf() // 사과 사라짐
                score = score + 100 // 점수 증가
                gameUI[7].text = string.format("%05d", score) // 점수를 다섯자리로 표시
            end
        end

        local moveApple = transition.to( apple, { time=2500, alpha=0.2, y=(player.y+500), onComplete = createApple } )
    end

    local onTimerComplete = function(event)
        createApple()
    end

 

Main 함수

  • 플레이어 캐릭터와 장애물(가시열매)를 배치하고, 오브젝트 간의 충돌효과를 위해 physics.addBody를 적용한다.
  • 사과와의 collision 이벤트를 관리하기 위해 플레이어 캐릭터에 destructible타입을 지정해주었다.
  • 이렇게 type을 지정해주면 오브젝트가 무슨 타입인지 식별하고, 타입별로 다른 액션을 취할 수 있게 된다.
  • 가시 열매에 collision이라는 EventListner를 추가해주었다. 이제 충돌을 인식할 수 있다.
    local player = display.newImageRect("Fruit/나며꾼.png", 300*0.6, 480*0.6)
    player.x, player.y = display.contentWidth/2-100, display.contentHeight/2+200
    player.alpha = 1

    physics.addBody(player, "static", {friction = 0, bounce = 1})
    player.type = "destructible"

    local thorn = display.newImageRect("Fruit/나며꾼가시.png", 90, 90)
    thorn.x, thorn.y = player.x+200, player.y/8
    thorn.alpha = 1

    physics.addBody(thorn, "dynamic", {friction = 0, bounce = 1})
    thorn:addEventListener("collision", thorn)

    // 만약 플레이어가 가시열매와 충돌한다면?

    thorn.collision = function(self, event)
        if(event.other.type == "destructible") then
            score = score - 20 // 점수 감소
            gameUI[7].text = string.format("%05d", score) // 점수를 다섯자리로 표시
        end
    end

 

✨소감

아무튼 이런 느낌

좋았던 점

  • 학부생 1학년 과정을 막 마친 수준에서 만들게 된 첫 게임이다. 루아 문법이 간단해서 진입장벽이 높지는 않았다.
  • 이제 겨우 C 문법 기초를 떼고 Java를 드문드문 배우는 수준이었던지라 API라는 것 자체에 익숙하지 않았다. 그렇기에 Solar 2D API Reference 사이트를 열심히 뒤져가며 한땀한땀(...) 만든 경험은 굉장히 값지다고 생각한다.
  • 캐릭터와 오브젝트, 시작화면과 결과화면까지 직접 그려서 게임에 적용해보는 과정이 큰 성취감을 느끼게 해주었다. 
  • 이 경험을 바탕으로 Unity 게임 제작에도 도전해볼 용기를 얻었다. (하지만 C# 앞에서 처참하게 패배했다.)

아쉬운 점

  • 다시 말하지만 막 Java를 배운지 얼마 되지 않은 시기에 만든 게임이다. 객체 지향 프로그램을 구현하지 못했던 탓에 Main 스크립트 파일에 모든 기능을 때려넣었고, Main 파일이 불필요하게 길고 복잡했다.
  • 자료구조데이터베이스 관련 지식이 없었기 때문에, 단순한 미니게임 이상의 수준을 구현할 수는 없었다.
  • 실력 부족으로 인해 누락된 기능이 몇 가지 있다. (결과 화면에서 처음으로 돌아가기, 키보드로 이동하기 등)
  • 이제는 이 기능들을 구현할 수 있을 정도의 실력은 되었으니, 기회가 된다면 부족한 부분을 보완해서 한 번 더 만들어보고싶다.  

+ Recent posts