월별 글 목록: 2022년 3월월

Spring 입문 3

AppConfig 리팩터링

현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다.


기대하는 그림

  • 각 메서드의 반환타입을 구현체가 아닌 인터페이스로 반환 타입으로 반환해야함을 잊지 말자
  • 모든 메서드는 public으로 작성한다.
    AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.

새로운 구조와 할인 정책 적용

처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자.
FixDiscountPolicy RateDiscountPolicy 어떤 부분만 변경하면 되겠는가?
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다


FixDiscountPolicy -> RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.
AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy -> RateDiscountPolicy
객체로 변경했다.
이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다.
클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.

좋은 객체 지향 설계의 5가지 원칙의 적용 (여기서 3가지 SRP, DIP, OCP 적용)

SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.


클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있음
SRP 단일 책임 원칙을 따르면서 관심사를 분리함
구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
클라이언트 객체는 실행하는 책임만 담당


DIP 의존관계 역전 원칙
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.


새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했다. 왜냐하면 기존
클라이언트 코드( OrderServiceImpl )는 DIP를 지키며 DiscountPolicy 추상화 인터페이스에
의존하는 것 같았지만, FixDiscountPolicy 구체화 구현 클래스에도 함께 의존했다.
클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경했다.
하지만 클라이언트 코드는 인터페이스만으로는 아무것도 실행할 수 없다.
AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트
코드에 의존관계를 주입했다. 이렇게해서 DIP 원칙을 따르면서 문제도 해결했다.


OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.


다형성 사용하고 클라이언트가 DIP를 지킴
애플리케이션을 사용 영역과 구성 영역으로 나눔
AppConfig가 의존관계를 FixDiscountPolicy RateDiscountPolicy 로 변경해서 클라이언트
코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다!

IoC, DI, 그리고 컨테이너
제어의 역전 IoC(Inversion of Control)
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다.
반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다. 예를 들어서 OrderServiceImpl 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.
프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl
도 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 이 아닌 OrderService
인터페이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사실도 모른체 OrderServiceImpl 은 묵묵히 자신의 로직을 실행할 뿐이다.
이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.


프레임워크 vs 라이브러리


프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit) 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.


의존관계 주입 DI(Dependency Injection)
OrderServiceImpl 은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.


정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는
애플리케이션을 실행하지 않아도 분석할 수 있다. 클래스 다이어그램을 보자
OrderServiceImpl 은 MemberRepository , DiscountPolicy 에 의존한다는 것을 알 수 있다.
그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl 에 주입 될지 알 수 없다.

동적인 객체 인스턴스 의존 관계
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.

객체 다이어그램

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입인스턴스를 변경할 수 있다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너, DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
  • 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다

스프링으로 전환하기

AppConfig를 스프링 기반으로 변경

  • AppConfig에 설정을 구성한다는 뜻의 @Configuration 을 붙여준다.
  • 각 메서드에 @Bean 을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다

MemberApp에 스프링 컨테이너 적용
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
– 작성한 AppConfig의 빈들을 스프링 컨테이너에 등록한 후 생성
MemberService memberService = applicationContext.getBean(“memberService”, MemberService.class);
– 컨테이너에서 등록된 스프링 빈들 중 AppCofing.memberService를 가져옴 (“가져올 메서드 이름”, “반환타입”)

스프링 컨테이너

  • ApplicationContext 를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )
  • 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다

컨테이너에 등록된 모든 빈 조회

모든 빈 출력하기

  • 실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.
  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
  • ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
    애플리케이션 빈 출력하기
  • 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력해보자.
  • 스프링이 내부에서 사용하는 빈은 getRole() 로 구분할 수 있다.
    — ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
    — ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
ac.getBean(빈이름, 타입)
ac.getBean(타입)
조회 대상 스프링 빈이 없으면 예외 발생
NoSuchBeanDefinitionException: No bean named ‘xxxxx’ available
(타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정)

스프링 빈 조회 – 상속 관계
부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.

BeanFactory vs ApplicationContext
BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • getBean() 을 제공한다.
  • 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다.
    ApplicationContext
  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 그러면 둘의 차이가 뭘까?
  • 애플리케이션을 개발할 때는 빈은 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.
  • 정리
  • ApplicationContext는 BeanFactory의 기능을 상속받는다.
  • ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다.
  • BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.

싱글톤 컨테이너

웹 애플리케이션과 싱글톤

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
  • 기존에 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다! ->메모리 낭비가 심하다.
    해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. -> 싱글톤 패턴

싱글톤 패턴
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다
참고: 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다

싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
    안티패턴으로 불리기도 한다.

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다!
    — 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    — 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
    — 가급적 읽기만 가능해야 한다.
    — 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!

금주의 IntelliJ 단축키

  • Ctrl + Alt + M : 리팩토링
  • iter : 집합의 각 요소들을 하나씩 꺼내주는 for문 생성
  • soupv : 변수 print 자동 작성
  • Ctrl + D : 드래그 범위 코드를 복사하여 밑에 붙임 (복제)
  • Ctrl + E : 최근 열었던 파일 목록 ( Enter 입력 시, 바로 전 파일 열람)
  • Ctrl + Shift + Enter : 현재 입력바가 어디 있든 다음 라인으로 이동
  • Shift x2 : 메서드, 클래스 등 검색 (Ctrl + N)

◆ 그 외 사항

포스팅 계획 – 3월 : 스프링 / 4월 : 정처기 실기(+중간) / 5월 : 스프링부트, MVC 모델링 / 6월 : 졸업 작품(+기말) / 7월 : 스프링부트 / 8월 ~ : 알고리즘과 CS, JPA, 스프링부트를 포함한 미정

  • 아직 해당 작성글에 틀린 부분이 많은 것으로 예상됩니다. 언제나 댓글 환영입니다
  • 그리고 이 글은 인프런 김영한 선생님의 Spring 로드맵 과정입니다

밥약(4)

다른 일이 바빠서 미루다가 다시 제작하게 되었습니다.

웹으로 제작하기위해 프론트 디자인을 만들어 봤습니다.

깔끔하지 않나요? 네 저도 알고 있어요

간단하게 Python Flask를 이용해서 만들어 보았네요

다음에는 시간표를 자동으로 만드는 코드를 제작하고 디자인해서 올리도록 하겠습니다

시외/고속 버스 AppleWallet Pass 제작 (4)

안녕하세요 오랜만에 글 쓰네요 하핫 ^^

놀고 있던 중에 슬픈 메일이 왔습니다.

아 ㅋㅋ 또 돈내야해? 아 ㅋㅋㅋ

저거 라이센스 끊기기 전에 완성 시키는걸 목표로 잡겠습니다.

QR코드 인식 때문에 애 먹던 중에 건우형이 구원의 손길을 내어 주심

보니까 이건 나도 찾아봤던건데;;; 하면서 에잉 이거 아닌데 했는데 알고 보니까 밑에 내가 원하는 기능이 있었음

진우 바봉 >___<

해서 해당 기능을 토대로 적용시켜보겠습니다 하핫

Kotlin 안드로이드 앱 프로그래밍 (4)

변수 선언
코틀린 내에서 변수 선언은 val , var 키워드로 선언합니다. val은 value의 줄임말로 초깃값이 할당되면 바꿀 수 없는 변수를 선언할 때 사용하고, var은 variable의 줄임말로 초깃값이 할당된 후에도 값을 바꿀 수 있는 변수를 선언할 때 사용합니다.
val(value) = 값 할당 시 바꿀 수 없는 변수 선언할 때 사용
var(variable) = 값 할당된 후에도 바꿀 수 있는 변수 선언할 때 사용

val(혹은 var) 변수명 : 타입 = 값 
val과 var 변수의 차이

val data1 = 10
var data2 = 10

fun main(){
    data1 = 20 //오류
    data2 = 20 //성공
}

data1과 data2에 변수를 선언하였고, data1은 val 키워드로 선언했으며 초깃값을 할당한 후 main() 함수에서 값을 변경하려고 시도합니다. 여기서 컴파일 오류가 발생합니다. val 키워드로 선언한 변수는 처음 할당한 값으로만 이용할 수 있습니다. 그런데 data2 변수는 var 키워드로 선언했으므로 값을 변경할 수 있습니다.
val = 초깃값 할당 후 변경불가
var = 초깃값 할당 후 변경가능 [위 내용 참고]

타입 지정과 타입 추론
변수명 뒤에는 콜론을 추가하여 타입을 명시할 수 있으며, 대입하는 값에 따라서 타입을 유추 할 수 있을 때는 생략 가능합니다.

val data1: Int = 10
val data2 = 10     -- (val data2: Int = 10) 유추 가능하면 생략가능

data1은 명시적으로 Int 타입을 선언한 것이며 data2는 대입한 값이 10 이므로 타입을 명시하지 않아도 자동으로 Int 타입이 됩니다. 즉 val data2: Int = 10이라고 선언한 것과 똑같습니다.

초깃값 할당
최상위에 선언한 변수나 클래스의 멤버 변수는 선언과 동시에 초깃값을 할당해야 하며, 함수 내부에 선언한 변수는 선언과 동시에 초깃값을 할당하지 않아도 됩니다. 물론 변수를 이용하려면 값을 할당하고 이용해야 합니다.

변수-클래스 멤버변수 선언과 동시에 초깃값을 할당
함수 내부에 선언한 변수 선언과 동시에 초깃값 할당하지 않아도 됨

val data1: Int //오류
val data2 = 10 //성공

fun someFun(){
    val data3: Int
    println("data3 : $data3")  //오류
    data3 = 10
    println("Data3 : $data3") //성공
}

class User{
    val data4: Int //오류
    val data5: Int = 10 //성공
}

**초깃값 할당하기**

초기화 미루기
변수를 선언할 시 초깃값을 할당할 수 없는 경우가 있습니다. 이때는 값을 이후에 할당할 것이라고 컴파일러에게 알려 주어야 합니다. lateinit나 lazy 키워드를 이용하여야 합니다.
lateinit 키워드는 이후에 초깃값을 할당할 것임을 명시적으로 선언합니다.

lateinit var data1: Int    //오류
lateinit val data2: String //오류
lateinit var data3: String //성공

lateinit 2가지 규칙
1. lateinit은 var 키워드로만 변수 선언 사용 가능합니다.
2. Int,Long,Short,Double,Float,Boolean,Byte 타입에는 사용할 수 없습니다.

lazy 키워드는 변수 선언문 뒤에 by lazy {} 형식으로 선언하며, 소스에서 변수가 최초로 이용되는 순간 중괄호로 묶은 부분이 자동으로 실행되어 그 결괏값이 변수의 초깃값으로 할당됩니다. lazy 문의 중괄호 부분을 여러 줄로 작성한다면 마지막 줄의 실행 결과가 변수의 초깃값이 됩니다.

val data4: Int by lazy{
    println("in lazy.....")
    10
}

fun main(){
    println("in main.....")
    println(data4 + 10)
    println(data4 + 10)
}

결과값
in main.....
in lazy.....
20
20

데이터 타입
코틀린이 제공하는 데이터 타입
코틀린의 모든 변수는 객체, 따라서 코틀린의 모든 타입은 객체 타입입니다. 정수를 다루는 타입이 Int인데 Int는 기초 데이터 타입이 아니라 클래스 입니다.

fun someFun(){
    var data1: Int = 10    
    var data2: Int? = null  // null 대입 가능

    data1 = data1 + 10
    data1 = data1.plus(10)  // 객체의 메서드 이용 가능
}

data1과 data2 변수를 Int 타입으로 선언했습니다. 만약 Int 타입이 기초 데이터 타입이라면 변수에 null을 대입할 수 없으며 메서드를 호출할 수도 없습니다. 하지만 코틀린의 모든 타입은 객체이므로 Int 타입의 변수에 10이라는 정수 뿐만 아니라 null을 대입할 수도 있습니다. 또 한 객체의 메서드도 호출할 수 있습니다.
** null은 객체가 선언만 되고 메모리 할당이 되지 않았음을 의미합니다. **

기초 타입 객체
(Int Short Long Double Float Byte Boolean)
기초 데이터를 객체로 표현하는 타입입니다. 정수를 표현하는 Int, Short, Long과 실수를 표현하는 Double, Float, 2진수를 표현하는 Byte, 그리고 true나 false를 표현하는 Boolean 타입이 있습니다.

val a1: Byte = 0b00001011

val a2: Int = 123
val a3: Short = 123
val a4: Long = 10L
val a5: Double = 10.0
val a6: Float = 10.0f

val a7: Boolean = true

파이썬 라이브러리 소개: diskcache

diskcache 는 말 그대로 disk 에 cache 데이터를 저장시켜주는 라이브러리 입니다.

일반 캐시와 deque 등 다양한 자료구조를 지원합니다.

기본적인 캐시 선언입니다.

from diskcache import Cache
cache = Cache()

다음과 같이 사용할 수 있습니다. 자료를 get/set 하고 만료기한을 설정할 수 있습니다.

cache.pop('alice')
# 1
cache.pop('dave', default='does not exist')
# 'does not exist'
cache.set('dave', 0, expire=None, tag='admin')
# True
result = cache.pop('dave', expire_time=True, tag=True)
value, timestamp, tag = result
value
# 0
print(timestamp)
# None
print(tag)
# admin

데이터베이스를 샤딩하는 fanout cache 도 지원합니다. 샤딩이 되기 때문에 쓰기가 겹칠 확률이 줄어듭니다. (읽기는 동시에 해도 상관 없음) 아래 예시는 샤드를 4개 하고 타임아웃이 1초인 경우입니다.

from diskcache import FanoutCache
cache = FanoutCache(shards=4, timeout=1)

함수 리턴값을 캐시할 수 있습니다.

from diskcache import FanoutCache
cache = FanoutCache()

@cache.memoize(typed=True, expire=1, tag='fib')
def fibonacci(number):
    if number == 0:
        return 0
    elif number == 1:
        return 1
    else:
        return fibonacci(number - 1) + fibonacci(number - 2)
print(sum(fibonacci(value) for value in range(100)))

Kotlin 안드로이드 앱 프로그래밍 (3)

코틀린의 등장 배경
코틀린은 젯브레인스(JetBrains)에서 오픈소스 그룹을 만들어 개발한 프로그래밍 언어입니다. 코틀린은 2011년에 처음 공개되었으며 2017년 구글에서 안드로이드 공식 언어로 채택함에 따라 알려지게 되었습니다. 코틀린은 러시아의 섬에서 이름을 따와 명명되었습니다.
코틀린으로 안드로이드 앱개발을 할수있는 이유는 자바의 JVM에 기반을 둔 언어이기 때문입니다.

코틀린은 자바와 유사하지만 다른 언어입니다. 확장자도 (.java / .kt) 로 다릅니다. 하지만 코틀린 컴파일러가 .kt 파일을 컴파일하면 자바 바이트 코드가 만들어집니다. 즉, 개발자는 자바와 다른 코틀린으로 코드를 작성하지만 컴파일하면 자바 클래스가 만들어지고 이를 JVM이 실행합니다. 이 이유로 코틀린이 자바를 대체할 목적으로 만든 언어라고 소개되고 있다.

코틀린으로 안드로이드 앱 개발을 하게 된다면 장점은?
1) 표현력과 간결함
2) 안전한 코드
3) 상호 운용성
4) 구조화 동시성

코틀린의 파일 구성
코틀린의 파일 확장자는 .kt 입니다. 다음 아래의 예시는 코틀린의 대체적인 파일 구성입니다.
(User.kt)

package com.example.test3 (패키지)

import java.text.SimpleDateFormat (임포트)
import java.util.*

var data = 10 (변수)

fun formatData(date: Date): String{ (함수)
    val sdformat = SimpleDateFormat("yyyy-mm-dd")
    return sdformat.format(date)
}

class User{ (클래스)
      var name = "hello"
      
      fun sayHello(){
          println("name : $name")
      }
}

package 구문은 이 파일을 처음 컴파일했을 때 만들어지는 클래스 파일의 위치를 나타냅니다. 소스파일에서는 맨 처음 한 줄로 선언합니다. 이 파일은 com/example/test3 폴더에 생성됩니다. 그런데 package 이름은 kt 파일의 위치와 상관없는 별도의 이름으로도 선언할 수 있습니다. 예를 들어 위의 User.kt 파일이 com/example/test3에 있더라도 package ch3 처럼 선언할 수 있습니다. 물론 이렇게 선언했을 때 컴파일된 클래스 파일은 ch3 폴더에 생성됩니다.

import 구문은 package 구문 아래에 여러 줄 작성할 수 있습니다. 그리고 import 문 아래에 변수, 함수, 클래스를 선언할 수 있습니다. 변수와 함수는 클래스 안 뿐만 아니라 클래스 밖에도 선언할 수 있습니다. 그리고 어떤 파일에 선언한 변수, 함수, 클래스를 다른 코틀린 파일에서 참조할 때 두 파일을 같은 package로 선언했다면 import 없이 사용할 수 있습니다.

[코틀린 파일명을 클래스명과 다르게 선언해도 되는 이유?]
코틀린 파일명과 파일 내에 선언한 클래스명과는 상관이 없습니다. 코틀린은 객체지향만을 목적으로 한 언어가 아니라서 그렇습니다. 객체지향만 지원하는 언어라면 모든 것이 클래스로 묶여야 하지만 코틀린은 변수, 함수 등을 클래스로 묶지 않고 최상위에 선언할 수 있으므로 파일명과 클래스명은 아무런 상관이 없습니다.

[코틀린의 변수, 함수를 클래스로 묶지 않고 선언한다면 자바에서 이용할 때 문제는?]
아무런 문제 없습니다. 코틀린에서는 변수, 함수를 최상위에 선언할 수 있으므로 하나의 코틀린 파일에 선언한 멤버를 다른 코틀린 파일에서 이용할 때는 변수, 함수, 클래스에 각각 접근하면 됩니다. 그런데 자바에서는 모든 것을 클래스로 묶어야 합니다. 즉, 자바에서는 최상위에 클래스만 선언할 수 있으며 변수나 함수는 최상위에 선언할 수 없습니다. 따라서 코틀린에서 최상위로 선언한 변수나 함수를 자바에서 이용할 수 없을 것 처럼 보입니다. 하지만 코틀린 컴파일러가 가능하게 해줍니다.

코틀린 소스를 테스트하는 방법
코틀린 소스 파일의 main()함수를 실행하는 코드입니다. 안드로이드 스튜디오에서 실행하는 방법입니다.
프로젝트 실행 -> 탐색 창에서 java 디렉터리 아래 패키지명으로 된 경로를 마우스 오른쪽으로 클릭하고 [New -> Kotiln Class/File -> File]을 선택한 후 적당한 이름으로 코틀린 파일을 만듭니다. 테스트할 코틀린 소스 파일에는 다음처럼 main() 함수가 있어야 하며, 이 소스 파일을 실행하면 main() 함수가 자동으로 실행됐다가 끝나면 프로그램이 종료 됩니다.

fun main(){                                                       [Test.Kt]
    println("hello world")
}

안드로이드 스튜디오에서 탐색 창에서 main() 함수가 있는 코틀린 파일을 마우스 오른쪽으로 선택한 후 [Run ‘TestKt’] 메뉴를 누릅니다. 메뉴 이름에서 ‘TestKt’는 파일명에 ‘Kt’자가 붙은 것 입니다.

실행 메뉴를 선택하면 안드로이드 스튜디오 아래쪽 [Run] 창에 실행 결과가 출력됩니다.

[토이 프로젝트 – Spring boot] 여행 플래너 GoAnyWhere

내가 사용하려고 만드는 여행 플래너 프로젝트 가능하면 무중단 배포를 곁들인..

보기 좋고 편한 여행 플래너들이 많이 있지만 대부분 모바일 어플리케이션 환경으로 제공되고 있으며, 웹 상에서 친구와 함께 공유 할 수 있도록 구현해보려고 합니다.

공부와 병행하는 프로젝트이니 스프링 이니셜라이져로 환경 세팅을 하는 것이 아닌 그레들로 시작하여 스프링부트 환경을 조성할 예정입니다.

개발 환경

  • Spring boot
  • Java 8
  • Gradle
  • AWS – EC2, RDS, Codedeploy etc..
  • Maria DB

구현하고자 할 기능

  • 로그인 – 구글 API 사용
  • 개인 플래너 기능 저장
  • 플래너 – 지도, 한국관광청 정보 제공 API
  • 그룹 기능 – 플래너 내에서 멤버 추가
  • AWS 배포

공부하면서 하기에 프로젝트가 조금 늦어질 수 있다는 점 양해 부탁드립니다.

Spring 입문 2

스프링 입문 공부이지만 스프링을 사용하지 않은 순수 자바로만 구현하는 것을 시작으로 한다.
그 이후 하나씩 스프링을 활용한 코드를 추가하고, 환경이 변경되는 과정을 밟으며 다형성과 DI 등을 공부함에 목적이 있다.

◆ 작업 환경

  • JAVA 11
  • IDE : Intelli J
  • Web Framework : Spring
  • OS : Windows

◆ 프로젝트생성 (환경설정)

  1. start.spring.io에서 Gradle Project, Vers = 안정적인 최신판, Artifact = core, Packaging = Jar, Java = 11 사용
  2. 압축풀고 build.gradle을 open (build 코드 변경 시 reload 필수)
  3. setttings에서 build Tools의 Gradle의 run, test를 IntelliJ로 변경 (직접 실행으로 빠른 속도)

◆ 비즈니스 요구사항과 설계

  • 회원
    회원을 가입하고 조회할 수 있다.
    회원은 일반과 VIP 두 가지 등급이 있다.
    회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    회원은 상품을 주문할 수 있다.
    회원 등급에 따라 할인 정책을 적용할 수 있다.
    할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
    할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
    ( 도메인 협력관계, 클래스 다이어그램, 객체 다이어그램 등을 프로그램 설계 시, 필수적으로 작성)

저장소 구현 방법 1. 내부 메모리 사용 (DB의 변경 가능성을 열어둠)

  1. domain 생성
    -1. member 패키지 생성
    -2. Grade를 enum을 생성 (BASIC, VIP)
    -3. Member 엔티티 생성 (Long id, String name, Grade grade)
  2. repository 생성
    -1. repository 패키지 생성
    -2. MemberRepository 인터페이스 생성
    void save(Member), Member findById(Long)
    -3. 상속받아 MemoryMemberRepository 구현체 생성
    private static HashMap store = new HashMap<>();
    (내부 메모리를 사용하므로 HashMap형태의 store 생성 – 동시성 이슈로 인해 concurrent HashMap을 사용해야 하나 테스트용이므로 생략)
    save() : store.put(member.getId(), member)
    findById() : return store.get(memberId)
  3. service 생성
    -1. service 패키지 생성
    -2. MemberService 인터페이스 생성
    void join(Member), Meber findMember(Long)
    -3. 상속받아 MemberServiceImpl 구현체 생성
    MemberRepository Type으로 MemoryMemberRepository 객체 생성 ( m = new)
    join() : m.save()
    findMember() : return m.findById(id)
  4. Member join JUnit Test
    -1. test 파일에 member 패키지 생성
    -2. MemberServiceTest 작성
    new MemberServiceImpl
    @Test
    void join(){
    // given : new Member()
    // when : mService.join() mService.find()
    // then : Assertions.assertThat(member).isEqualTo(findMember) – 두 객체가 같은지 비교
    (Assertions는 org.asserj.core… 을 사용) << 주문도메인 사진>>
  5. 할인 정책 인터페이스와 정액 정책 구현체
    -1. discount 패키지 생성
    -2. DiscountPolicy 인터페이스 작성
    int discount(Member, Int) : return 할인 금액
    -3. 상속받아 FixDiscountPolicy 구현체 작성
    discountFixAmount = 1000 // 반환할 고정 할인 금액
    discount() : if(member.getGrade() == Grade.VIP) {1000} else {0} // enum은 ==을 통해 동일 비교
  6. 주문 엔티티
    -1. order 패키지 생성
    -2. Order 엔티티 작성
    private memberId, itemName, itemPrice, discountPrice
    생성자 + Getter,Setter
  7. 주문 서비스 인터페이스와 구현체
    -1. order 패키지에 OrderService 인터페이스 작성
    Order createOrder(Long memberId, String itemName, int itmePrice)
    -2. 상속받아 OrderServiceImpl 구현체 작성
    미리 만들어둔 memberRepository와 discountPolicy의 구현체를 new로 생성
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
    Member member = memberRepository.findById(memberId); // member 찾기
    int discountPrice = discountPolicy.discount(member,itemPrice); // member 등급에 맞는 할인 금액 반환
    return new Order(memberId,itemName,itemPrice,discountPrice); // 주문 return
    }
    // 참고사항 : 여기서는 discout 정책과 관련되어 구현된 것이 없어, 차후 정책 변경이 유리 (SOLID 중요성)
  8. createOrder Test
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();
    @Test
    void createOrder(){
    //given : new Member(), join() // VIP
    //when : createOrder()
    //then : Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000)
    (VIP로 생성된 Member의 할인 금액이 1000원인지 비교)


객체지향 설계 원칙을 잘 준수했는지 확인하기위해 정액 할인 정책에서 정률 할인 정책으로 변경
(기존의 OrderServiceImpl에서 FixDiscountPolicy를 RateDiscountPolicy로 생성 – 다른 코드는 수정 X)

  1. 정률 할인 정책 구현체 작성
    -1. 기존의 DiscountPolicy 인터페이스를 상속하여 RateDiscountPolicy 생성
    -2. private int discountPercent = 10 : 할인률 10%
    -3. 정액 할인과 똑같이 작성
    if(member.grade==Grade.VIP){} else {}
    -4. Ctrl+Shift+T로 Test Class 생성
  2. Test
    DiscountPolicy discountPolicy = new RateDiscountPolicy();
    @Test // 정상 범위 테스트
    @DisplayName(“VIP는 10% 할인이 적용되어야 한다.”) // Test 결과 출력은 한글로 확인 가능
    void vip_o(){
    //given : new Member
    //when : discount
    //then : Assertions.assertThat(discount).isEqualTo(1000);
    (10% 할인된 금액인 1000원이 맞는지 비교)
    // 참고사항 : Grade를 BASIC으로 설정한 Member가 할인이 적용되지는 않았는지 Test 또한 필요함
  3. 기존 시스템에 할인 정책 교체 적용
    -1. OrderServiceImpl에서 할인 정책을 가져올 때, 단순히 변경
    new FixDiscountPolicy() –> new RateDiscountPolicy();

@ 문제점 발견
우리는 역할과 구현을 충실하게 분리했다. OK
다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. OK
OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다

  • 그렇게 보이지만 사실은 아니다.

DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를지킨 것 같은데?

  • 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고있다.
    — 추상(인터페이스) 의존: DiscountPolicy
    — 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
  • 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.

왜 클라이언트 코드를 변경해야 할까?
클래스 다이어그램으로 의존관계를 분석해보자.


지금까지 단순히 DiscountPolicy 인터페이스만 의존한다고 생각했다.


잘보면 클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐만 아니라
FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. 실제 코드를 보면 의존하고 있다! DIP 위반


중요!: 그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다! OCP 위반

어떻게 문제를 해결할 수 있을가?
클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도함께 의존한다.
그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
DIP 위반 추상에만 의존하도록 변경(인터페이스에만 의존)
DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
인터페이스에만 의존하도록 설계를 변경하자


인터페이스에만 의존하도록 코드 변경
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
인터페이스에만 의존하도록 설계와 코드를 변경했다.
그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?
실제 실행을 해보면 NPE(null pointer exception)가 발생한다.


해결방안
—이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.—

◆ 관심사의 분리

AppConfig 등장
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자

  1. Appconfig 작성 (애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는
    별도의 설정 클래스)
  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
    MemberServiceImpl
    MemoryMemberRepository
    OrderServiceImpl FixDiscountPolicy
  • AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
    MemberServiceImpl MemoryMemberRepository
    OrderServiceImpl MemoryMemberRepository , FixDiscountPolicy

설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존하지 않는다!
단지 MemberRepository 인터페이스만 의존한다.
MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서결정 된다.
MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다


객체의 생성과 연결은 AppConfig 가 담당한다.
DIP 완성: MemberServiceImpl 은 MemberRepository 인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

◆ 정리
AppConfig를 통해서 관심사를 확실하게 분리했다.
AppConfig는 공연 기획자다.
AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다. 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
이제 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.
OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.

◆ 금주의 IntelliJ 단축키

  • java파일에서 psvm을 치면 자동으로 작성
    public static void main(String[] args) { }
  • sout : System.out.println()
  • Alit + Insert : 생성자와 Getter,Setter는 물론 toString() 또한 자동 생성 가능
    (구현체 자체를 출력 시, 작성된 toString()이 호출하여 print()가능
  • Ctrl + Shift + T : 해단 메서드 Test Class 자동 생성
  • Ctrl + E : 프로젝트 내 파일이나 메서드 검색을 통한 이동

◆ 그 외 사항

포스팅 계획 – 3월 : 스프링 / 4월 : 정처기 실기(+중간) / 5월 : 스프링 / 6월 : 졸업 작품(+기말) / 7월 : 스프링 / 8월 ~ : 알고리즘과 CS, JPA, 스프링부트를 포함한 미정

  • 아직 해당 작성글에 틀린 부분이 많은 것으로 예상됩니다. 언제나 댓글 환영입니다
  • 그리고 이 글은 인프런 김영한 선생님의 Spring 로드맵 과정입니다

렌파이 프로젝트 01

팀원들이랑 하는 프로젝트 입니다.

팀원은 스토리 작가,UI&UX 디자이너, 그림작가, 프로그래머 2명입니다.

아직 베타버전 제작중이니 배포는 금지합니다.

스텐딩 cg 작업 중

캐릭터 컨셉 작성중

코드 작성 등장인물 정의, 배경정의, 스토리 적용, 효과 적용

# 등장인물 정의
init python:
    shin = Character('김신우')
    yeon = Character('이하연')
    park = Character('박윤호')
    yun = Character('이하윤')

# 배경 정의
image bg black: # 효과를 위한 검정 화면
    im.FactorScale("bg/black.png", 1.5)
    yalign 0.5

image bg dark:
    im.FactorScale("bg/dark.png", 1.5)
    yalign 0.5

image bg room:
    im.FactorScale("bg/room.png", 1.5)
    yalign 0.5

image bg livingroom:
    im.FactorScale("bg/livingroom.png", 1.5)
    yalign 0.5

image bg cafe:
    im.FactorScale("bg/cafe.png", 1.5)
    yalign 0.5

image bg street:
    im.FactorScale("bg/street.png", 1.5)
    yalign 0.5

image bg corridor:
    im.FactorScale("bg/corridor.png", 1.5)
    yalign 0.5

image bg school:
    im.FactorScale("bg/school.png", 1.5)
    yalign 0.5

###################################################################################
############################### 등장인물 이미지 정의 ###############################
###################################################################################

# 이하연
image yeon normal:
    im.FactorScale("character/yeon.png",0.4)
    yalign -0.1

image yeon smile:
    im.FactorScale("character/yeon.png",0.4)
    yalign -0.1

image yeon panic:
    im.FactorScale("character/yeon.png",0.4)
    yalign -0.1

# 이하윤
image yun normal:
    im.FactorScale("character/yun.png",0.4)
    yalign -0.1

# 트랜지션

define fade = Fade(1.5, 1.0, 1.5) # 1.5초 페이드 아웃 -> 1.0초 정지 -> 1.5초 페이드 인
define fadeShort = Fade(0.5, 1.0, 0.5)


# Prologue
label start:

    scene bg dark with dissolve
    # 어두운 음악 on

    "인생은 항상 선택의 연속이었다."
    "찰나의 선택은 항상 인생에 큰 영향을 끼쳤으며, 늘 그 선택에 후회를 하였다."

    "???" "행복했어. 좋은 부모님을 만났고 좋은 사람들과 좋은 시간을 보내왔으니까…."

    "선택 이후에는 늘 책임의 시간이 찾아왔다."
    "하지만, 나는 단 한 번도 그 책임에 대한 각오를 한 적이 없었다."

    "???" "그리고 처음으로 사랑하는 사람을 찾았어. 그러니까. 조금의 후회도 없어. 덕분에 결심을 할 수 있었던 것 같아."

    "어쩌면 각오를 해도 달라지는 것이 없었지 않았을까?"
    "늘 선택에 따른 결과는 비수가 되어, 날아와 나의 심장에 박혔다."
    "그렇게 또 하나의 지울 수 없는 상처만이 내 몸에 새겨질 뿐이었다."
    "더 좋은 선택을 할 수 있지 않았을까?"
    "아니, 어떤 선택을 했어도 결과에 후회를 했었을 것이다."
    "하지만, 그렇게 생각하지 않는다면, 나 자신을 부정할 것만 같았다."

    "???" "고마워…. 그리고 사랑해."

    "그녀를 잡아야 했다."
    "이대로 가다가는 그녀는 분명 사라질 것이었다."
    "하지만, 나는 잡을 수 없었다."
    "왜냐하면, 이 결과는 오로지 내가 선택한 결말이었으니까."
    "되돌릴 수는 없다."

    "???" "그럼 안녕."

    "점점 시야가 어두워진다."
    "그녀의 마지막 모습이었다."
    "끝까지 지켜봐야 하지만, 몸이 말을 듣지 않았다."
    "희미해지는 의식 속에서 그녀의 미소가 보였다."
    "동시에 극심한 후회가 밀려들기 시작했다."
    "잘못된 선택이었다."
    "더 좋은 선택을 할 수 있었을지도 모른다."
    "제길…. 그녀를 잡아야 한다."
    "하지만, 아무리 발버둥 쳐도 그녀는 점점 더 시야에서 멀어져갔을 뿐이었다."
    "그렇게 완전히 정신이 끊어졌다."

    # 어두운 음악 off

    ##############################################################################################################

    scene bg room with fade
    # 밝은 음악 on

    play sound "audio/se/thud.mp3"
    with vpunch

    pause(0.7)
    shin "아이 씨…."

    "무슨 꿈을 꾼 것인지 정신을 차리니 침대가 아닌 바닥에 자빠져있는 나는 극심한 두통을 느끼고 있었다."
    "분명 꿈을 꾼 것 같은데, 기억이 나지 않았다. 그것도 아주 중요한 꿈이었는데…."

    shin "에휴. 모르겠다."

    "떠올리는 것을 포기한 나는 자리에서 일어나서 방 밖으로 나왔다."

    scene bg black with blinds
    play sound "audio/se/footstep.mp3"
    pause(1.1)
    scene bg livingroom with blinds

    "올해 17살로 고등학교에 입학하는 나는 부모님의 카페 알바를 도우면서 기타를 치는 너튜버를 하고 있었다."
    "최근 구독자 30만 명을 달성하면서 나름 큰 유튜버를 운영하고 있었던 나는 주목받는 것을 싫어했기에 유튜버로서의 신분을 숨긴 채로 평범한 학생으로서 살아가고 있었다."

    "윤신혜" "신우야. 오늘 엄마랑 아빠가 어디 가거든? 홀로 카페 봐줄 수 있겠니?"

    "어미니의 목소리에 머리를 박박 긁으면서 주방으로 걸어온 나는 한숨을 크게 내쉬었다."

    shin "오늘 새로운 알바생 온다며, 어떡하려고?"
    "윤신혜" "이제는 우리보다 카페 일에 대해서 더 잘 알면서 그래?"
    shin "그래도 사장님이 있는 거랑 느낌이 다르지."
    "윤신혜" "오늘 중요한 행사니까. 부탁 좀 할게."

    "현재 부모님은 노랑재단이라는 봉사단체에서 상당히 주요한 직책을 맡고 계신다."
    "덕분에 제법 집과 카페를 많이 비우기도 해서 최근에는 내가 카페에 많은 관여를 하고 있었다."
    "물론 그렇다고 부모님이 아예 손을 놓고 계시는 것은 아니지만, 최근에는 재단에서 문제가 생겨서 그런지 내가 더 많이 카페 일에 관여하고 있었다."

    shin "뭐…. 알았어요. 다녀오세요."
    "윤신혜" "그래. 고마워."

    "어머니와 아버지가 집에서 나가자, 나는 밥상에 차려진 밥을 챙겨 먹고는 옷을 입은 후에 카페가 있는 1층으로 내려갔다."

    scene bg black with blinds
    play sound "audio/se/footstep.mp3"
    pause(1.1)
    scene bg cafe with blinds

    "전체적으로 말끔한 인테리어로 이루어진 부모님의 카페 ‘Lastory’는 막 손님이 몰리는 가게는 아니었지만, 단골이 여러 명 있어서 나쁘지 않은 흑자를 거두고 있었다."

    "단골손님" "어! 신우야 오늘도 왔다."
    shin "오…. 형 오늘도 오셨네요. 최근에는 안 바빠요?"
    "단골손님" "급한 일들은 저번에 다 처리해서 최근에는 조금 여유롭지."

    "오픈한지 대략 1분 만에 단골손님을 맞이한 이후로 점차 손님들이 들어오기 시작했다."
    "생각보다 많은 손님의 수에 나는 정신없이 일을 이어나가고 있었다."

    # @여기에 종 효과음을 넣으면 어떠려나?

    yeon "늦어서 죄송합니다!"

    "갑작스러운 목소리에 한참 커피를 타던 나는 고개를 돌렸고 한 예쁜 소녀의 모습이 눈에 들어왔다."

    show yeon smile with dissolve # @이전 대화에 등장하는 건?

    "긴 흑발 포니테일에 루비같이 빛나는 붉은 눈동자를 가진 그녀는 새하얀 피부와 다소 날카로운 턱선과 날 선 코와 커다란 눈이 고양이가 생각나는 그런 얼굴을 가지고 있었다." # @일러가 있으니 주인공 감상만으로 충분할듯
    "예쁘고 귀여운 외모를 가졌다는 감상이 절로 드는 그녀는 싱긋 웃으면서 내게 인사를 해왔다."

    show yeon normal
    yeon "오늘부터 이곳에서 일하게 된, 이하연이라고 해요. 잘 부탁드려요!"
    shin "아…. 하연씨군요. 반갑습니다. 부모님이 급히 볼일이 생기셔서 오늘은 제가 카페를 맡고 있습니다."
    yeon "아! 사장님의 아드님이셨군요."
    shin "네. 잠시 안쪽으로 들어와 주실래요?"

    "상당히 활발한 성격을 가진 하연씨의 모습에 나는 내심 안심하면서 열쇠를 들고 그녀를 안쪽 스태프실로 안내했다."

    show yeon panic
    yeon "손님 많은 것 같은데 괜찮아요?"
    shin "괜찮아요. 형." # @하연이를 형이라고 부르는 거 같은데...

    "걱정하는 하연씨의 말에 나는 입가에 미소를 지으면서 카운터 기준으로 바로 건너편에 있는 단골손님이자 아는 형을 불렀다." # @형을 부르는 거만 묘사하는 건? 그리고 스태프 실로 형을 부른 건가?

    "단골손님" "왜?"
    shin "추가로 손님 오시면 전화 좀 해주실래요?"
    "단골손님" "그래. 갔다 와." # @갔다 와? 어딜??
    shin "네. 부탁할게요. 들어갈까요?" # @바로 장면을 전환해도 괜찮을 듯.
    show yeon smile
    yeon "네!"

    show yeon normal
    "다소 높은 하연의 텐션은 주변 사람을 피곤하게 할 수도 있겠지만, 카페 알바로서는 크게 나쁠 것이 없었기에, 오히려 장점이라고 생각했다." # @대사를 좀 더 혼잣말처럼 바꾸는 건?

    shin "커피와 기타 간식들은 제가 만들고 있으니, 하연씨는 카운터와 중간중간 청소 그리고 서빙을 부탁드릴게요." # @간식보다는 디저트라는 표현이 좀 더 카페사장아들같지 않을려나?
    yeon "주의사항은 없나요?"
    shin "저희는 단골손님들이 많아서 간혹 ‘먹던 걸로’라고 하시는 분들이 있어요. 그분들의 경우에는 이름을 묻고 저에게 알려주시면 제가 알아서 음료를 가져다드릴게요." # @"이름을 묻고"는 빼도 괜찮지 않을까?
    show yeon smile
    yeon "네. 알겠습니다!"

    show yeon normal
    "그 후 본격적인 업무가 시작되었다. 평소보다 제법 사람이 많았기에, 제법 힘들만도 했지만, 하연씨는 상당히 능숙하게 일을 이어나갔다." # @굳이 일러를 넣어야할까?
    "친절하고 상냥한 성격과 더불어 예쁜 외모여서 그런지 단골손님들도 매우 마음에 들어 하는 모습이었다."

    shin "하연씨. 전에 다른 카페에서 일하신 적, 있어요?"
    yeon "아뇨. 딱히 없어요. 아마 알바도 여기가 처음일 거예요."
    shin "오…. 놀랍네요. 상당히 능숙하셔서 분명 경험이 있는 줄 알았어요."
    show yeon smile
    yeon "천직인가 봐요." # @천직이란 잘 안 쓰이는 표현보다는 "저랑 잘 맞나봐요."나 "적성에 맞나봐요." 같은 표현이 좋지 않을까?

    show yeon normal
    "좋은 일꾼과 함께 일을 하다 보니 별문제 없이 영업을 이어나갔다." # @이 부분도 굳이 일러를 넣어야할까?
    "그렇게 어느새 밤이 되었고 카페 문을 닫아놓고 마무리 청소를 하던 나에게 하연씨가 다가왔다." # @여기에 다가오는 듯한 연출로 일러를 넣으면 될 거 같은데.

    show yeon smile
    yeon "수고하셨어요!"
    shin "하연씨도 첫날부터 상당히 바빴는데 수고하셨어요."
    show yeon normal
    yeon "혹시 나이가 어떻게 되세요?"

    "갑작스러운 질문이었지만, 그렇게 중요하다고 생각하지 않았기에, 나는 순순히 대답했다." # @나도 너무 갑작스러운데

    shin "이제 고등학교에 입학해요."
    show yeon smile
    yeon "오..! 17살이에요? 저도 그래요." # @"오..!"보단 "아 진짜요? 저도 17살인데."가 좀 더 한국스럽지 않으...려나?

    show yeon normal
    "계속 봤음에도 불구하고 하연씨의 미소는 여전히 매력적이었다."
    "무표정을 짓고 있으면 고양이상이어서 그런지 조금 무서울 것 같기도 한데, 계속 웃고 있어서 그런지 그런 느낌은 조금도 들지 않았다."

    yeon "저…. 혹시 말 놓아도 괜찮아요?"
    shin "네. 상관없어요. 오히려 일할 때, 편할 것 같네요."

    "딱히 사장대리로서 알바에게 갑질할 생각도 그에 맞는 대우를 받을 생각도 없었기에 나는 흔쾌히 받아들였고 이에 이하연은 입가에 미소를 지었다." # @일러로도 알 수 있으니 "흔쾌히 받아들였다."까지만 쓰는 건?

    show yeon smile
    yeon "신우야! 수고했어."
    shin "그..그래 수고했어."

    "어느 정도를 각오를 했지만, 막상 이름으로 불리자, 조금 낯간지러운 느낌이 들었다."

    hide yeon with dissolve

    "그렇게 하연이 사라지고 말끔하게 정리된 카페를 보면서 나는 크게 한숨을 내쉬었다."

    shin "오늘도 어떻게든 보냈나…."

    # 밝은 음악 off

    ##############################################################################################################

    scene bg room with fade

    "다음날, 차가운 공기를 느낀 나는 곧바로 잠에서 깼다."
    "잠깐 침대에서 뒤척이면서 잠을 몰아낸 후 몸을 일으켰다."
    "잠결에는 그다지 의식하지 않았지만, 어느 정도 정신이 드니 찬바람이 들어오고 있다는 것을 느꼈다." # @상황 설명만 주구장창 늘여놓는 것보단 "으음..."같은 대사를 넣어주는 편이 좋을 듯

    scene bg black with blinds
    play sound "audio/se/footstep.mp3"
    pause(1.5)
    scene bg livingroom with blinds

    "부스스한 머리를 대충 정리한 채로 밖으로 나온 나는 창문이 조금 열린 것을 확인하고는 한숨을 내쉬었다." # @상황 설명만 하면 될 듯(ex: 밖으로 나와보니 역시 창문이 조금 열려있었다.)

    shin "아 문 좀 닫고 다니라니까."

    "아마 부모님이 제대로 닫지 않고 갔을 것이다."
    "완전히 창문을 닫고 잠금을 건 후, 주방으로 가서 베이컨과 계란후라이를 구워서 내려놓고는 토스트기에서 빵을 꺼냈다."

    "아나운서" "월요일 아침 출근길 매우 춥습니다. 서울의 현재 기온은 3도로 제법 추운 날씨가 예상되는 가운데…."

    "3월임에도 불구하고 꽃샘추위가 기승을 부리고 있어서, 밖은 상당히 추웠다."
    "두툼한 패딩과 새로 산 교복을 입고는 곧바로 현관문을 나섰다."

    scene bg black with blinds
    play sound "audio/se/doorlock.mp3"
    pause(1.7)
    play sound "audio/se/door_open.wav"
    pause(1.7)
    scene bg street with blinds

    # 잡음 효과음
    # 밝은노래 on

    "오늘은 처음으로 고등학교에 입학하는 날이다."
    "통학로에는 나와 똑같은 ‘유선 고등학교’ 교복을 입은 학생들이 학교로 향하고 있었다."
    "가던 중, 무거운 짐을 들은 할머니를 보자 뛰어서 다가가 말을 걸었다." # @상황 설명 외에 의성어나 의태어가 대사로 들어가는 편이 미연시스러울 듯

    shin "할머니, 어디까지 가세요?"
    "할머니" "아냐. 괜찮어. 학생 바쁠텐디 어여 가." # @사투리가 어색하여 임의로 수정함.
    shin "아직 시간 많아서 괜찮아요. 제가 도와드릴게요."
    "할머니" "학생이 참 착하구먼. 저기 버스 정류장까지 부탁혀." # @사투리가 어색하여 임의로 수정함.

    "괜한 오지랖일 수도 있지만, 나는 이러한 행동이 일종의 병과 같은 존재였다."
    "부모님 때문에 생긴 봉사병이라고 할까?" # @너무 번역체같은 데ㅋㅋㅋㅋ
    "하여튼 이런 분들을 보고 있으면 가만히 있을 수가 없었다."
    "덕분에 항상 착하다는 이야기를 많이 들었지만, 그만큼 호구라고 한 소리를 들을 때도 있었다."

    park "야! 김신우. 또 이러고 있냐?"

    "할머니의 짐을 내려다 준, 나에게 다가온 남자는 박윤호로 초등학교부터 같이 지낸 동네 친구였다." # @좀 더 문단을 나눌 필요가 있을 듯
    "성격이 매우 관종같지만, 생긴 것은 제법 준수해서 나름대로 인기가 있었다."
    "운 좋게 같은 고등학교에 붙게 되어서, 오늘도 같이 등교를 하고 있었다."

    shin "시끄러. 잘 들리거든?" # @이 부분도 뭔가 어색함. 시끄럽게 말하고 있다는 상황 설명이 필요할 듯
    park "오랜만에 봤는데 왜구루냥."

    "찰거머리처럼 달라붙는 이 자식은 나와 다른 녀석을 대하는 태도가 달랐다."
    "그렇다고 다른 사람에게 차갑게 대하거나, 그러지는 않았다." # @굳이 지금 넣어야하나?

    shin "새학기부터 힘이 넘치네."
    park "당연하지!"

    # 잡음 효과음

    "등굣길을 따라 천천히 걷던 도중, 갑자기 사람들의 수군거리는 소리가 들려왔다."
    "이에 윤호가 나에게 붙으면서 작은 소리로 말했다." # @굳이 넣을 필요가 있으려나?

    park "와. 이하윤이야. 이하윤."
    shin "이하윤? 그게 누군데."
    park "나도 친구들에게 들었는데, 우리 옆 중학교 있잖아. 거기 전교회장 출신이라던데?"
    shin "세카여중?" # @ㅁㅊ 중학교 이름 왜이럼 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

    "우리가 다녔던 중학교는 ‘유선 중학교’였고 그 근처에 ‘세카여자 중학교’라고 꽤 유명한 사립 여자 중학교가 있었다."
    "국내에서 상당히 유명한 중학교로 다른 학교와 달리 추천을 받은 학생만 들어갈 수 있는 학교였다." # @주인공은 모르는 눈치인데, 윤호의 입으로 푸는 편이 좋지 않으려나?

    shin "그런 사람이 왜 여기로 오는 거야? 보통 거기 출신들 다 서울로 가지 않아?"
    park "몇 명은 우리 학교로 오긴 하는데, 그중 한 명이 걔인가 봐. 특이하네. 전교회장 정도라면 유명 고등학교 들어갈 텐데." # @이 부분은 두 부분으로 나눠서 말하는 편이 좋을 듯.

    "‘유선 고등학교’도 자율형공립고등학교로 제법 이름이 있는 학교이긴 했지만, ‘세카여자중학교’에 비하면 다소 네임드가 부족했다." # @이 부분도 주인공은 모르는 부분인데 독백으로 넣는 건 좀 어색하지 않으려나?

    park "자세한 이유는 나도 몰라. 하여튼 저기있네, 저기."

    "학생들이 많아서 제대로 보이지 않았지만, 윤호는 필사적으로 이하윤을 보기 위해서 노력했다."
    "이에 나도 괜히 궁금해져서 조심스럽게 사람들을 헤쳐나갔다."
    "그러자, 이하윤의 모습이 드러났다." # @일러로 표현되는데 굳이 넣을 필요는 없어 보임.

    # 밝은노래 off
    show yun normal with dissolve

    "허리까지 길게 내려진 긴 흑발에 붉은색 커다란 눈동자를 가진 다소 차가운 인상은 예쁘다는 생각이 들면서도 범접하기가 힘든 그런 아우라를 내뿜고 있었다." # @일러가 있으니 주인공의 개인적인 감상만 있으면 될 듯.

    park "오…. 괜히 사람들이 예쁘다고 하는 게 아니네."
    shin "그..그러게."

    "예쁜 것이라 별개로 이하연과 상당히 비슷하다는 인상이 들었다."
    "게다가 이름까지도 상당히 비슷하기에, 혹시 쌍둥이가 아닌가라는 생각이 들 정도였다."
    "물론 분위기 자체가 워낙 달라서 구별하는 데 있어서 어려움은 없었지만 말이다."

    shin "윤호야. 저 사람 쌍둥이 형제가 있어?"

    "갑작스러운 질문에 윤호는 생각에 잠긴 표정으로 고민하더니 이내 고개를 저었다."

    park "아니 들어본 적은 없어. 나도 자세히 아는 것은 아니라서. 근데 왜?"
    shin "아냐. 그냥 어제 카페에 온 사람하고 상당히 비슷하다고 생각해서."
    park "이하윤하고 비슷한 사람이라고? 와. 겁나 부럽네."
    shin "부러운 것도 많다." # @부럽다는 표현은 딱 한 번 나왔는데 좀 어색한듯.

    "그렇게 우리는 등굣길에 중학교 친구 3명을 더 만난 상태에서 학교 안으로 들어갔다." # @말이 좀 어색한데, "친구 3명과 추가로 합류했고, 그 상태로 학교에 도착했다."가 좀 더 자연스럽지 않음?

    hide yun # @여기에서? 좀 더 뒤에서 하는 편이 좋을 듯.
    # 밝은노래 off @진작에 꺼진 상태입니다만?

    ##############################################################################################################

    # 밝은노래 on
    scene bg corridor with fade

    "학교에 들어오자마자, 우리들은 게시판으로 향했다."
    "학생들이 우글거리는 이곳은 반 배정표가 걸려있었다."
    "3년 만에 느끼는 두근거림에 떨리는 마음으로 배정표에 도착했다."

    shin "3반이네. 윤호, 너도야."
    park "오 개꿀이네."

    "기쁜 마음에 우리는 하이파이브를 했다."
    "안 그래도 모르는 사람투성이인데, 한 명의 동지가 얼마나 큰지 알 수 있었다."

    "이치룡" "하…. 씨. 혼자네, 어카냐."
    "정용훈" "나도 그래도 한석이랑 같은 반이네." # @나도 그래도는 뭐야
    "김한석" "그러게. 다행이네."

    "치룡이만 제외한다면 각자 두 명씩, 같은 반에 붙은 모양이었다."
    "한숨을 길게 내쉬는 치룡이를 달래주고는 각자 배정된 반으로 향했다."

    scene bg black with blinds
    play sound "audio/se/footstep.mp3"
    pause(1.1)
    # 드르륵 문 열리는 소리
    scene bg school with blinds

    "‘1-3’이라고 1학년 3반 교실로 들어오자, 소수의 학생이 눈에 들어왔다."
    "당연히 다 모르는 얼굴이었고 상당히 어색한 분위기였기에, 우리는 조용히 앉았다."

    park "와이씨…. 분위기 미쳤네."

    "책상에 얼굴을 박은 채로 얼굴을 찡그리는 윤호를 보면서 피식 웃음이 나왔다."

    shin "진짜 어색하기는 하네."

    "몇몇 아이들만 아는 사이로 보이는 애들끼리만 이야기를 나누고 있었고 대다수의 아이들은 스마트폰만 바라보고 있었다." # @'만'이 연속으로 나와 좀 어색해보임.
    "나도 윤호가 없었다면, 저 아이들과 다를 것이 없었겠지."

    # 드르륵 문 열리는 소리

    "그렇게 조곤조곤 이야기를 나누던 도중, 드르륵 문 열리는 소리에 모두가 문으로 시선이 고정되었다."
    "그리고 그곳에는 이하윤이 있었다."

    show yun normal with dissolve

    "다시 봐도 예쁘다는 것이 느껴지는 얼굴은 정석적인 우등생의 느낌을 주었다."
    "다른 아이들도 그렇게 느낀 것인지 모두 그녀를 멍하게 바라보고 있었다."
    "그런 시선이 익숙한 것인지 아니면, 눈치채지 못한 것인지 그녀는 조용히 자리에 가서 앉더니, 문제집 한 권을 꺼냈다." # @독백으로 상황설명안 주구장창 늘여놓으니 게임적으로 어색함.

    park "야야. 김신우 대박. 이하윤이 우리 반인가 봐!" # @아까 배정표를 봤을텐데 몰랐을까?
    shin "생각지도 못했네."
    park "와. 개이득이네."
    shin "뭐가 개이득이야. 사귀는 것도 아니고."
    park "친해질 기회가 있다는 거잖아."

    "윤호의 말이 이해가 안 가는 것은 아니었지만, 어깨를 으쓱거리면서 그의 등을 가볍게 때렸다." # @어깨를 으쓱거리면서... <- 이부분은 빼도 될 듯.

    shin "말이라도 걸 수 있으면 다행이지."
    park "펙트 아프다…."

    "확실히 이하윤이 풍기는 분위기는 여간, 분위기가 아니었다."
    "특히 공부를 하고 있는 지금과 같은 경우에는 절대 못 건들었다."

    shin "여차하면 나중에 쉬는 시간에 말이라도 걸어봐."
    park "뭐야. 신우 너도 관심 있냐?"
    shin "계속 그쪽으로 몰아가지 마라."

    "황당함에 비소를 지으면서 다시금 윤호의 등을 때렸다."
    "제법 내 손이 매웠는지 박윤호는 얼굴을 찡그리면서 말했다."

    park "아파. 이시키야."

    "그렇게 노가리를 까는 사이, 선생님이 들어오셨고 그 이후로는 입학식의 정석처럼 이어졌다."

    hide yun # @너무 늦게 없애는 건 아닌지?
    # 밝은노래 off
    scene bg black with blinds
    pause(1.1)
    scene bg school with blinds

    "별문제 없이 입학식이 끝나고 첫날 수업이 이어졌다."
    "대부분의 선생님이 OT라고 하면서 가르치는 것보다는 자기소개 위주로 수업을 진행했다."
    "자리는 원하는 대로 앉을 것이라고 생각했는데, 한가지 변수가 생겼다."

    # 밝은노래 off
    # 사고브금 on

    "담임 선생님" "이제부터 자리를 바꿀 거예요. 신학기인 만큼 두루두루 친하게 지내야 하니까. 다들 너무 나쁘게 생각하지 마세요." # @문단을 나눌 필요가 있어보임.
    "반 아이들" "네에?"
    "반 친구A" "에이 쌤. 그건 오바죠."

    "갑자기 자리를 바꾼다는 소식에 나와 윤호는 서로를 바라보면서 ‘망했다’라는 표정을 지었다." # @좀 더 주인공의 속마음이 대화처럼 들어가면 좋을듯.
    "둘이 있어도 이 어색한 분위기를 견디기 힘든데, 다른 사람하고 있게 된다면 도저히 견딜 수가 없을 것이었다."
    "상상만 해도 몰려오는 어색함에 온몸이 쪼그라드는 느낌이었다."

    "담임 선생님" "이미 결정된 사항이에요."

    "첫인상은 상당히 자상하고 부드러운 선생님이었는데, 이런 면에 있어서는 상당히 엄한 부분이 있으셨다."
    "결국 선생님의 진행 아래 자리 바꾸기가 진행되었다."
    "그리고 생각지도 못한 결과가 나와버렸다." # @이 부분에서도 의성어나 의태어같은 부분이 들어가면 괜찮을듯.

    with fade # @굳이?

    "뽑기 결과 놀랍게도 내 옆자리에 앉은 것은..."

    show yun normal with dissolve

    "이하윤이었다." # @여기도

    yun "안녕"
    shin "어..어 안녕."

    "바로 옆에 있다는 것은 그만큼 얼굴도 더 잘 보인다는 것이었다."
    "새하얀 피부에 날카로운 얼굴선, 적당히 각진 코에 살짝 붉은 입술과 커다란 적색의 눈동자는 실로 감탄이 나오는 외모였다."
    "그리고 무엇보다 이하연과 비슷하다는 감상을 지울 수가 없었다." # @여기서 더 의심하는 편이 더 좋지 않을까요?

    shin "분위기는 다른 데 말이지..."
    yun "응? 뭐라고 했어?"
    shin "아냐! 그냥 혼잣말이였어."

    "생각한 것을 겉으로 내뱉은 것이었는지,
    고개를 돌려 의문에 가득 찬 표정을 짓는 이하윤의 모습에 당혹감을 느낀 나머지 양손을 들어 올리면서까지 고개를 저었다." # @당혹감이 아니라 위화감이 느껴지는게 "
    "그 후 이하윤과 딱히 이야기를 나누지 않았다."
    "아니 정확히는 나눌 수가 없었다."
    "방금 그런 모습을 보였는데, 어떻게 말을 걸겠는가?" # @개인적인 견해로 여기서 잠을 잤다가 깨서 종이 울리는게 개연성 부분에서 맞다고 생각합니다!

    # @여기서 페이드를 넣고 자연스럽게 넘어가는 대사를 넣어주면 좋겠다고 생각합니다.
    # 사고브금 off
    # 밝은노래 브금 on
    # 학교 종소리 효과음 on

    "이윽고 수업이 끝나고 쉬는 시간이 되자. 여자애들이 이하윤의 곁으로 다가왔다."

    "윤소진" "하윤이 맞지?"
    yun "어 맞아 너는 소진이지?"
    "윤소진" "오 알고 있었구나!"
    yun "물론이지."

    "이하윤은 딱히 말을 잘하는 것은 아니었지만, 조용히 남의 말을 잘 들어주고 적당한 반응을 하다 보니 금세 여러 여자 애들과 친해졌다."
    "괜히 라세여중에서 인기가 많았던 것이 아니었다." # @세카여중과 라세여중이 똑같은곳?

    # @여기 부분에서 박윤호가 다가오는 묘사를 말해주면 좋겠다.

    park "와…. 김신우 존나 부럽네."
    shin "부럽긴 뭐가 부러워. 여러모로 가시방석이야."
    park "저 모습 보면 그래도 우리가 생각하는 것보다는 괜찮은 것 같은데?"
    shin "그렇긴 한데…."

    "즐겁게 이야기를 나누는 이하윤의 모습은 확실히 처음 이미지와 다르게 다른 여자애들과 다를 것이 없었다."

    shin "그래도 쉽지는 않아."
    park "이럴 때는 여자였으면 조금 편했을 듯?"
    shin "취향 존중할게."
    park "미친놈…. 그래서 오늘 끝나고 피방 기?"
    shin "오늘은 시간 있긴 해."

    "이하윤에 대한 이야기도 잠시 우리들도 원래 하던 이야기로 화제가 돌아왔다."
    "우연히 짝이 된 것은 놀랍지만, 그렇다고 크게 접점이 있지는 않았다."
    "그리고 굳이 무리하여 접점을 가질 필요도 없다 생각했다."

    hide yun

    # 밝은노래 브금 off
    # 교실 배경 off

    with fade

    # 교실 방과 후 배경 on
    # @객관적인 표현 사용 시간 표현.

    "그렇게 시간이 끝나고 방과 후가 되었다." # @접속사 많음.
    "그리고는 첫 번째 당번이 되어서 뒷정리를 하고 있었다."
    "그런 나와 같이 정리를 하고 있는 사람은…." # @비주얼 노벨에서 소설처럼 쓰기에는 어울리지 않습니다.

    show yun normal with dissolve

    yun "거기 칠판지우개 좀 줄래?"
    shin "어 여기."

    "놀랍게도 이하윤이었다."
    "우리가 당번이 된 이유는 1분단 가장 앞자리에 배정받았기에, 이렇게 된 것이었다."
    "물론 당번 일을 시작한 지 10분이 지났음에도 우리의 사이는 어색했다."

    yun "대충 거의 다 했지?"
    shin "어 다했어. 짐만 옮기면 될 것 같아."

    "교실 앞쪽에 한 개의 상자가 있었다."
    "이하윤은 먼저 상자로 다가가더니, 이내 상자를 들어 올렸다."
    "그러나, 상자가 조금 무거웠던 것인지 팔이 덜덜 하면서 떨렸다."
    "이에 빠르게 다가가 그녀의 상자를 대신 들어주었다. "

    shin "도와줄게."
    yun "어..어 고마워."

    # @여기서 박스 드는 소리

    "상당히 묵직한 무게는 제법 운동 좀 한다고 생각했던 나도 무거울 정도였다."
    "이하윤이 들기에는 무리라고 할 정도로 말이다."
    "상자를 든 채로 이하윤과 같이 창고로 들어온 나는 적당한 곳에 내려놓았다."
    "계속해서 침묵을 이어가는 상태에서"

    return

2022년 예비창업패키지 서류 준비

2021년 생애최초 청년창업 지원사업을 졸업 후 후속 지원 사업인 예비창업패키지 준비하고 있었습니다. 2월 말 2022년 예비창업패키지 지원 공고 메일을 받고 미뤄온 사업계획서를 이제 준비하게 되었습니다.

예비창업패키지 준비를 미루고 있다 이번 지원 사업에서 사업계획서 목차가 변경이 된 것을 확인하였습니다. 미뤄온 저 자신을 반성하면서 변경된 항목에 맞게 서류를 작성하였습니다.

17일 까지 작성한 서류를 계속 읽어보고 수정하여 제출할 예정입니다.

이거 제출하면 5월까진 쉰다 휴