일단 IntelliJ 를 이용해서 서버를 구축할 것이다.

Hibernate JPA 와, MyBatis 둘 중에 하나를 선택해야 하는데.

 

이번 기회에 MyBatis 와 조금 더 친밀해져 보려고 한다.

 

일단 둘의 차이점을 알고 가보자.

 

JPA 란 무엇인가?

 

JPA(Java Persistent API)

  JPA는 여러 ORM 전문가가 참여한 EJB 3.0 스펙 작업에서 기존 EJB ORM이던 Entity Bean을 JPA라고 바꾸고 JavaSE, JavaEE를 위한 영속성(persistence) 관리와 ORM을 위한 표준 기술이다. JPA는 ORM 표준 기술로 Hibernate, OpenJPA, EclipseLink, TopLink Essentials과 같은 구현체가 있고 이에 표준 인터페이스가 바로 JPA이다.

  ORM(Object Relational Mapping)이란 RDB 테이블을 객체지향적으로 사용하기 위한 기술이다. RDB 테이블은 객체지향적 특징(상속, 다형성, 레퍼런스, 오브젝트 등)이 없고 자바와 같은 언어로 접근하기 쉽지 않다. 때문에 ORM을 사용해 오브젝트와 RDB 사이에 존재하는 개념과 접근을 객체지향적으로 다루기 위한 기술이다.

 

https://blog.woniper.net/255

 

[JPA] JPA란 무엇인가?

JPA란 무엇인가? JavaSE 환경에서 JPA 설정 및 CRUD JavaEE 환경(Spring)에서 JPA 설정 및 CRUD @OneToOne, 1:1 관계 매핑 @OneToMany / @ManyToOne, 1:N / N:1 관계 매핑 @ManyToMany, N:M 관계 매핑 Entity 객체..

blog.woniper.net

한글로 풀어 쓰면, 자바 영속성 API(Application Programming Interface) 이다. API 라는 것은 말 그대로, 어플리케이션 프로그램이 다른 프로그램을 사용할 수 있게 해주는 인터페이스를 뜻한다. 

 즉 "자바로 영속성을 다루는 프로그램"의 "인터페이스"를 제공하는 것이 바로 JPA 다.

이 것은 말 그대로 "인터페이스" 이므로, 이를 구현하는 것에는 여러가지 프로그램이 있다. 가장 대표적인 것 중 하나가 바로 Hibernate다.

 

자. 간단히 정리하자면. JPA 는 결국 인터페이스이고, 이 인터페이스를 구현한 것 중 하나가 하이버네이트이다.

이렇게 따지면 MyBatis도 JPA 를 구현한 것 아닌가?? 할 수 있겠지만. JPA 는 풀 네임만 보면 오해의 여지가 있지만, 사실 ORM 을 위한 표준 기술을 의미한다. 

 ORM 은 Object Relational Mapping 의 약자다. 

 

그럼 MyBatis는 무엇이냐? Sql Mapper다. 이 두 개의 차이점은 간단히 말하자면,

 

ORM 은 Object를 기반으로 DB 가 생기고,

SQL Mapper는 DB 구축은 해 놓고, 그 DB에서 꺼내온 데이터를 Object에 꾸겨 넣는 것이다.

 

 하여간 MyBatis를 사용하기로 하고, 한번 IntelliJ를 이용해 간단히 스프링 부트 프레임워크를 구축해보자.

 

IntelliJ 에선 그 전에 살펴봤던, Spring initializer를 간단히 사용할 수 있게 해준다.

 

 

대충 원하는대로 서버 정보를 입력 해준다.

 

웹 서버이기 때문에 Spring Web Server 를 체크 해준다.

DB 설정

DB 는 간단히 H2 를 사용하자. 나중에 이 부분은 MariaDB 로 바꿀 것이다. (mySql)

H2 란 무엇인가?

 

H2DB

H2DB는 자바 기반의 오픈소스 관계형 데이터 베이스 관리 시스템(RDBMS )입니다.

H2DB는 서버(Server) 모드임베디드(Embedded) 모드인메모리 DB 기능을 지원합니다. 물론 디스크 기반 테이블을 또한 생성할 수 있습니다.

또한 브라우저 기반의 콘솔모드를 이용할 수 있으며, 별도의 설치과정이 없고 용량도 2MB(압축버전) 이하로 매우 저용량 입니다. DB자체가 매우 가볍기 때문에 매우 가볍고 빠르며, JDBC API 또한 지원하고 있습니다.

SQL 문법은 다른 DBMS들과 마찬가지로 표준 SQL의 대부분이 지원됩니다.

이러한 장점들 때문에 어플리케이션 개발 단계의 테스트 DB로서 많이 이용됩니다.



출처: https://dololak.tistory.com/285 [코끼리를 냉장고에 넣는 방법]

인 메모리로 사용할 시, 서버가 종료하면 자동으로 데이터도 사라진다. 메모리 상에만 데이터가 떠 있게 된다.

 

캐시 서버로도 쓰일 수 있겠지만, 그 방면에서는 key-value 형식의 noSql 강자인 redis 가 있으므로 포지션이 애매해진다.

현재 가장 적합한 포지션은 "테스트용 경량 RDBMS" 정도다.

 

Developer tool 에서는 간단히 Lombok plugin 만 설정해 두었다. Lombok 에 대해서는 따로 언급하지 않겠다.

 

이제 필요한 디펜던시들이 설정되고, spring initializer 가 이 설정을 기반으로 pom.xml 을 만들어주고, 필요한 소스 파일 및 폴더 구조를 모두 만들어 준다. 조금 기다리면 메이븐 기반으로 라이브러리들이 설치된다.

 

이제 mybatis 와 h2로 기본적인 설정을 진행 해보자.

 

일단 dataSource 설정을 미리 해주어야 한다. DataSource 설정이란 무엇인가?

 

https://gmlwjd9405.github.io/2018/05/15/setting-for-db-programming.html

불러오는 중입니다...

 

여기에 아주 자세히 설명 돼있다.

 

이 링크를 타면 만나는 DAO, DTO, Entity Class 의 차이도 한 번 읽으면 좋을 것 같다.

 

https://gmlwjd9405.github.io/2018/12/25/difference-dao-dto-entity.html

불러오는 중입니다...

가장 핵심은 이것이다.

 

DAO 는 Database 에 Access 할 때 사용하는 Object다. Spring 기준으로 보면 보통 @Repository 가 붙은 놈들이다.

DTO 는 말 그대로 Data를 주고 받을 때 사용하는 Object이며

Entity Class 는 DB 와 Data를 직접 주고 받을 때 사용하는 놈이다.

 

왜 DTO와 Entity Class를 나누는가?

 

-View Layer와 DB Layer의 역할을 철저하게 분리하기 위해서

-테이블과 매핑되는 Entity 클래스가 변경되면 여러 클래스에 영향을 끼치게 되는 반면 View와 통신하는 DTO 클래스(Request / Response 클래스)는 자주 변경되므로 분리해야 한다.

-Domain Model을 아무리 잘 설계했다고 해도 각 View 내에서 Domain Model의 getter만을 이용해서 원하는 정보를 표시하기가 어려운 경우가 종종 있다. 이런 경우 Domain Model 내에 Presentation을 위한 필드나 로직을 추가하게 되는데, 이러한 방식이 모델링의 순수성을 깨고 Domain Model 객체를 망가뜨리게 된다.

또한 Domain Model을 복잡하게 조합한 형태의 Presentation 요구사항들이 있기 때문에 Domain Model을 직접 사용하는 것은 어렵다.
즉 DTO는 Domain Model을 복사한 형태로, 다양한 Presentation Logic을 추가한 정도로 사용하며 Domain Model 객체는 Persistent만을 위해서 사용한다.

 

즉, DB 와 직접 통신하는 놈은 최대한 군더더기 없이 유지하는 것이 좋다.

데이터 전달 상, DB 통신과는 전혀 상관없는 필드와 메서드들이 덕지덕지 붙을 수 있기 때문에 Entity Class와 DTO 클래스를 분리한다.

DTO 클래스는 상~당히 Entity Class 와 유사하다.

 

다시 DataSource 로 돌아가보자.

 

위에 DAO 보이는가? 

DAO 는 Data Access Object가 Database를 만나기 위해 거쳐야 하는 두 계단이 있다.

JDBC Template과 JDBC Driver다.

 

아래의 설명은 Datasource 링크의 내용이다.

 

# DataSource 란?
DataSource는 JDBC 명세의 일부분이면서 일반화된 연결 팩토리이다. 즉 DB와 관계된 connection 정보를 담고 있으며, bean으로 등록하여 인자로 넘겨준다. 이 과정을 통해 Spring은 DataSource로 DB와의 연결을 획득한다.

DataSource는 JDBC Driver vendor(Mysql, Oracle 등) 별로 여러가지가 존재한다.

DataSource가 하는 일
 - DB Server와의 기본적인 연결
 - DB Connection Pooling 기능 (* 아래 참고)
 - 트랜젝션 처리


DataSource의 구현 예시
 - BasicDataSource (선택!)
 - PoolingDataSource
 - SingleConnectionDataSource
 - DriverManagerDataSource

 

DataSource 는 커넥션 정보를 담는다. 즉 DB Server와의 기본적인 연결을 담당한다.

 

 

따라서 우리는 이 Datasource 설정을 해줄 필요가 있는 것이다. 

mybatis 와 h2를 이용하기 위해서 아래의 링크들을 참고할 것이다.

"참고" 이기 때문에, 아래의 링크와 다른점도 많다. (다른점이 더 많을 수도 있다.)

 

http://tech.javacafe.io/2018/07/31/mybatis-with-spring/

 

Spring Boot에서 mybatis 설정하기

스프링 프로젝트를 진행할때마다 DB와의 연동은 필수적으로 들어가게 된다. 최근에는(드디어) JPA를 이용해 프로젝트를 많이 진행하지만, 여전히 SQL을 이용한 직관적인 매핑방식도 많이 사용되고 있다. 단순한 프로젝트라면 SpringJdbc를 이용해서도 충분히 SQL을 구성할 수 있다. 하지만 실무환경에서는 다양한 경우의수가 발생하고 이에 따른 SQL도 함께 변경되어야 하고, 이러한 복잡한 SQL이 필요한 지점에서 MyBatis의 동적 SQL관리방법은 프로

tech.javacafe.io

 

 

https://www.technicalkeeda.com/spring-tutorials/spring-4-jdbctemplate-annotation-example

 

Spring 4 JdbcTemplate Annotation Example

In this tutorial, we will learn how to connect to the database and execute CRUD SQL queries using Spring 4 JdbcTemplate. Java base configuration is used to load the JdbcTemplate, here we are not using XML base configuration for JdbcTemplate.

www.technicalkeeda.com

왜 굳이 이렇게 두 개를 참고하냐면,

개인적인 공부를 위해서 Datasource 설정을 두 가지 방법으로 해볼 생각이기 때문이다.

 

첫 번 째로, spring boot 의 자동설정 기능을 이용해본다.

두 번 째로, @Configuration 으로 Datasource Bean 을 JAVA 코드로 만들어 본다.

 

 

음... 따라해보자.

일단 spirng boot 기본 설정을 이용해 datasource 설정을 해준다.

application.properties 파일에 다음과 같이 입력 해주자.

 

spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=wp
spring.datasource.password=password123
spring.datasource.driver-class-name=org.h2.Driver

 

비밀번호는 테스트이므로 아무거나 입력했다. 나중에는 바꿔줘야 한다.

 

예시

 

 

H2는 인메모리 DB로 애플리케이션이 동작하는 경우에만 DB가 함께 동작한다. Spring이 시작하는 시점에 scheme이 생성되도록 /src/main/resource/scheme.sql을 생성하여 주고 아래에 다음과 같이 table 생성 SQL과 예제데이터를 추가하여 주자.

 

mybatis 설정 관련 첫 번째 링크에서 Spring이 시작되는 시점에 DB의 scheme 을 생성하기 위해 /resource 아래에 scheme.sql 을 생성해 준다고 돼있다. 링크를 타고 가서 스프링 공식 문서를 봐보자.

 

Spring Boot can automatically create the schema (DDL scripts) of your DataSource and initialize it (DML scripts). It loads SQL from the standard root classpath locations: schema.sql and data.sql, respectively. In addition, Spring Boot processes the schema-${platform}.sql and data-${platform}.sql files (if present), where platform is the value of spring.datasource.platform. This allows you to switch to database-specific scripts if necessary. For example, you might choose to set it to the vendor name of the database (hsqldb, h2, oracle, mysql, postgresql, and so on).

 

공식 문서에 의하면, Spring Boot 는 자동으로 스키마를 생성하고, 데이터를 생성할 수 있다. 

기본 루트 클래스 패스 (/src/main/resources/schema.sql 이겠지?) 위치에 있는 scheme.sql 에 기입된 DDL scripts 를 실행해 schema 를 생성하고, data.sql 에 기입된 DML scripts 를 실행해 "데이터" 를 생성한다.

 DDL 과 DML 의 차이는 여기서는 설명하지 않겠다. (다들 알겠지.)

 

 

일단 DDL 을 이용해 schema.sql 을 작성 해야 하는데 ,먼저 schema.sql 파일을 생성하자.

 

schema.sql

 

 

여기서 DDL 을 사용해 scheme.sql 안을 채워야 하는데.

 

https://www.fun-coding.org/mysql_basic3.html

 

데이터베이스 기본 (MySQL): SQL DDL(Data Definition Language) 이해 및 실습 - 잔재미코딩

3.2.2 테이블 삭제¶ 기본 문법 (DROP TABLE 테이블명)mysql> DROP TABLE [IF EXISTS] 테이블명; IF EXISTS 옵션은 해당 데이타베이스 이름이 없더라도 오류를 발생시키지 말라는 의미

www.fun-coding.org

너무 좋은 링크가 있어서, 여길 참고해서 만들어 보도록 하겠다.

 

링크의 내용과 여러 Goolgle 검색을 토대로 DDL 을 작성 해준다. 덤으로 DML 도 살짝 얹었다.

 

CREATE TABLE TEXT_DIARY(
    ID INT PRIMARY KEY AUTO_INCREMENT,
    NAME VARCHAR(20) NOT NULL,
    CONTENT TEXT NOT NULL,
    DATE DATE DEFAULT CURRENT_DATE,
    CREATE_DATE DATETIME DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO TEXT_DIARY (NAME, CONTENT) VALUES ('오늘의 일기', '오늘은 공부를 했다.')

 

일단 간단한 테스트 TABLE 을 만들어 보도록 했다.

TEXT만 입력 가능한 간단한 도메인을 설정 해보자. 

 

TEXT_DIARY 의 PRIMARY_KEY 는 ID 이며  INT 타입으로, 자동으로 증가하는 값을 가진다,

이름을 갖고 있으며 20자 이내여야 한다, 내용은 길게 들어갈 수 있으므로 적당히 TEXT 로 해놓는다,

CREATE_DATE 와 DATE 를 갖는다. 이 둘을 구분한 이유는 상식적으로 일기는 항상 당일에 작성하지는 않기 때문이다.

 

아직은 테스트 단계이기 때문에 전체적인 틀을 잡는다기보다는 간단한 테스트용 테이블을 위와 같이 만들어 보았다.

 

예시

링크에서 시키는대로 엔트리 포인트가 되는 클래스에다가 @MapperScan 을 붙여준다.

 

mapper를 스캔할 수 있도록 설정하자. SpringBoot Configuration 파일상단에 아래와 같이 프로젝트 패키지의 경로를 넣자. 이렇게 하여 프로젝트 하위에 존재하는 Mapper들을 빈으로 등록할수 있게 된다.

 

@MapperScan을 붙인 모습

링크를 보면 모델의 type-alias 를 사용하기 위한 application.properties 파일 수정이 있지만, 그 부분은 제외하도록 하자.

 

DML 을 이용해 나름 데이터를 입력까지 했으니, 일단 꺼내서 확인해보자.

Entity class를 만들자. 이를 위해 package를 새로 따서, 그 안에 TextDiary 라는 Entity Class를 만들 것이다.

 

 

 

그리고 데이터를 입력 해준다.

 

package com.wpapp.voice_diary_server.model;

import lombok.Data;

import java.util.Date;

@Data
public class TextDiary {
    private Integer id;
    private String name;
    private String content;
    private Date date;
    private Date createDate;
}

 

대충 요런 형태로 만들었다.

 

그리고 DAO 역할을 해줄 mapper 인터페이스와(repository), 이 DAO 를 다루는 서비스, 그리고 그 서비스를 다루는 Rest Controller 를 만들 것이다.

 

구조는 이렇게 된다.

 

Rest Controller - 서비스 - DAO(Repository and mapper)

 

결과적으로 다음과 같은 구조가 된다.

 

 

 

TextDiaryMapper 는 다음과 같다.

 

package com.wpapp.voice_diary_server.mapper;

import com.wpapp.voice_diary_server.model.TextDiary;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface TextDiaryMapper {
    List<TextDiary> findAll();
}

 

왜 findAll() 만 있냐면, 당장 schema.sql 에서 넣은 데이터를 확인하고 싶기 때문이다. 

이름이 왜 findAll() 인가? 그냥 말 되게 하면 되는데 getAll() 이던 findAll() 이건...

하지만 프로그래머는 convention 을 따르는 것이 중요하기 때문에, 다음에 이 DAO method 의 관례에 대해 알아보자.

 

 

그리고 TextDiaryMapper.xml 을 만들어 준다.

 

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.wpapp.voice_diary_server.mapper.TextDiaryMapper">
    <select id="findAll" resultType="com.wpapp.voice_diary_server.model.TextDiary">
        SELECT
        ID, NAME, CONTENT, DATE, CREATE_DATE as createDate
        FROM TEXT_DIARY
    </select>
</mapper>

이 파일은 다음과 같은 위치에 있다.

 

이 mapper xml 파일을 제대로 읽어들이려면, application.properties 파일에 이 mapper xml 파일들의 위치를 설정해주어야 한다.

 

spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=wp
spring.datasource.password=password123
spring.datasource.driver-class-name=org.h2.Driver


mybatis.mapper-locations=classpath:/mapper/*.xml

그러면 application.properties 의 모양새는 위처럼 된다.

(classpath: 는 resources 다.)

 

 

TextDiaryService 는 다음과 같다.

 

package com.wpapp.voice_diary_server.service;

import com.wpapp.voice_diary_server.mapper.TextDiaryMapper;
import com.wpapp.voice_diary_server.model.TextDiary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TextDiaryService {
    @Autowired
    private TextDiaryMapper textDiaryMapper;

    public List<TextDiary> findAll() {
        return textDiaryMapper.findAll();
    }
}

 

솔직히 왜 있는줄 모르겠다.

이것 또한 관례를 따른 것인데, 이 관례도 워낙 옛날에 듣고 따라하는 것인지라 나중에 자세히 알아보도록 하자.

(서비스 - 리포지토리 분리)

 

-----

나중에 할 것

1. repository method 명명 관례(혹은 dao method)

2. service, repository 등의 구분 이유 및 관례

------

 

마지막으로 RestController

 

package com.wpapp.voice_diary_server.controller;

import com.wpapp.voice_diary_server.model.TextDiary;
import com.wpapp.voice_diary_server.service.TextDiaryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/text-diaries")
public class TextDiaryRestController {

    @Autowired
    private TextDiaryService textDiaryService;

    @GetMapping
    public List<TextDiary> findAllTextDiary() {
        List<TextDiary> diaries = textDiaryService.findAll();
        return diaries;
    }
}

 

이렇게 일사천리로 코드를 작성하고 localhost:8080/text-diaries 에 들어가보면 어찌 되는가?

 

시간이 이상하게 나온다.

 

...??

 

타임존이 다르다.

 

Date 가 JSON 으로 바뀌는 과정에서 뭔가 이상한 타임존의 변화가 있는 것일까?

일단 Date 를 LocalDate 와 LocalDateTime 으로 바꾸면 당장은 제대로 작동하게 된다.

 

TextDiary를 다음과 같이 바꾸었다.

 

package com.wpapp.voice_diary_server.model;

import lombok.Data;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
public class TextDiary {
    private Integer id;
    private String name;
    private String content;
    private LocalDate date;
    private LocalDateTime createDate;
}

 

이러니 제대로 작동한다. 왜 이러는 걸까?

당장은 다음의 과제로 남겨야 할 것 같다. 졸리기 떼문이다.

 

https://www.baeldung.com/java-set-date-time-zone

 

Set the Time Zone of a Date in Java | Baeldung

Learn how to set the time zone of a date in Java 7, Java 8 and Joda-Time

www.baeldung.com

일단 위의 링크와

 

https://www.baeldung.com/jackson-serialize-dates

불러오는 중입니다...

위의 링크를 남기고, 다음에 한번 왜 이러한 상황이 발생 했는지 정리하도록 하자.

 

 

이번에는 "초기 설정" 이기 때문에 세세히 정리를 해두었다. 

초기 설정은, 처음에 한 뒤로 "프로젝트를 다시 시작하지 않는 이상" 다시 안 할 가능성이 높기 때문에,

그만큼 덜 해보게 되고, 그만큼 한 번 하고나면 잊고 끝나기 마련이다.

 

따라서 초기 설정을 세세히 정리하고, 다시 복습하고자 요래 쓸데없이 장황하게 정리를 해놓았다.

 

미래의 나에게 도움이 되길 빈다.

블로그 이미지

맛간망고소바

,

이제부터 스프링 부트 서버를 "공부 및 제작" 할 것이다.

 

스프링부트는 차세데 스프링 프레임워크 이다. 스프링 부트는 개발 방식을 단순화한다.

스프링 부트는 탄탄하고 확장성 좋은 스프링 앱을 개발하는 방향으로 베스트 프랙티스를 따르도록 인도하는 독자적인 기술의 결집체이다.

 

아주 간단한 스프링 웹 애플리케이션도 J2EE 스택 및 스프링 프레임워크에 관한 몇 가지 개발 규칙은 지켜야 한다.

 

웹 아카이브(WAR) 파일 내부 구조는 다음과 같다.

 - WEB-INF 폴더는 반드시 필요하고, 하위에 lib, classes 폴더를 둔다.

    - lib 폴더엔 서드파티 라이브러리,

    - classes  폴더엔 웹 애플리케이션 클래스를 넣는다.

- 필요시 JSP, HTML, CSS, 이미지, 자바스크립트 파일을 넣는다.

- web.xml 파일에 디스패처 서블릿을 선언한다.

- <서블릿명>-servlet.xml 형식의 스피링 빈 구성 파일이 필요하다.

 

war 파일은 유틸리티로 패키징한다.

war 파일은 톰캣, 제티, 제이보스, 웹스피어 같은 "애플리케이션 서버 또는 컨테이너" 에 올려놓고 실행하거나, J2EE 앱을 배포하는 전용 서버를 따로 운용하기도 한다.

 

-- 웹 아카이브의 구조 및, 웹 아카이브로 패키징 해야 한다는 것, 그리고 이 war 파일을 실행하는 데 필요한 것. --

 

몇빈 선언 시점에 스프링 XML 스키마를 올바르게 교정하는 STS 같은 IDE 부터, 요맨 같은 외부 도구에 이르기까지, 구조를 바로 잡고 틀에 박힌 구성코드를 일일이 입력하지 않게 해주는 도구는 여럿 있다.

 

package com.example.wphomepage;

import com.example.wphomepage.journal.Journal;
import com.example.wphomepage.journal.mapper.JournalMapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class WpHomepageApplication {

    @Bean
    InitializingBean saveData(JournalMapper repo) {
        return () -> {
            repo.insert(new Journal("스프링부트 입문", "난 언제나 다시 복습하지", "05/26/2019"));
            repo.insert(new Journal("스프링부트 입문", "난 언제나 다시 복습하지", "05/26/2019"));
        };
    }

    public static void main(String[] args) {
        SpringApplication.run(WpHomepageApplication.class, args);
    }

}

 

 옛날에 작성했던 코드를 붙였다. 5월 26일날 복습했던 기본 코드다.

지금 다시 복습하니 3개월 만에 다시 보는 꼴이다.

 

여기서 public static void main 함수 부분을 보자.

 

SpringApplication.run 부분을 찾을 수 있다.

 

-SpringApplication 은 스프링 앱을 간편히 초기화하는 특별한 클래스이다. 

-스프링 부트로 애플리케이션을 개발하면 XML 구성 파일이 거의 필요 없다.

-여기서는 사용되지 않았지만, SpringApplicationBuilder 라는 연결형 빌더 API 가 있는데, 여러 앱 컨텍스트를 계층화할 수 있다.

앱을 좀 더 세세히 다룰 수 있는 장치라고 보면 된다.

 

 

-스프링 부트는 '독자적인' 기술이므로, 스프링 부트를쓰면 웹 애플리케이션이든 단독형(standalone) 앱이든 어떤 타입의 앱이더라도 올바르게 개발할 수 있다.

-스타터폼으로 의존체를 알기 쉽게 구성, 관리할 수 있다. 가령 웹 애플리케이션을 개발할 때는 메이븐 폼 또는 그레이들 빌드 파일에 spring-boot-starter-web 의존체를 추가한다.

 

 

일단 거두절미하고 스프링 부트 CLI 를 설치해보자.

설치하는 방법은 여러가지다. 그 중 sdkman 을 이용해 설치하는 방법은 다음과 같다.

 

curl -s get.sdkman.io | bash

 

===============

CURL

curl 은 command line 용 data transfer tool 이다. download/upload 모두 가능하며 HTTP/HTTPS/FTP/LDAP/SCP/TELNET/SMTP/POP3 등 주요한 프로토콜을 지원하며 Linux/Unix 계열 및 Windows 등 주요한 OS 에서 구동되므로

여러 플랫폼과 OS에서 유용하게 사용할 수 있다. 또 libcurl 이라는 C 기반의 library 가 제공되므로 C/C++ 프로그램 개발시 위의 protocol 과 연계가 필요하다면 libcurl 을 사용하여 손쉽게 연계할 수 있다.

libcurl은 PHP, ruby, PERL 및 여러 언어에 바인딩되어 있으므로 사용하는 언어나 개발환경에 맞게 libcurl 을 사용할 수 있다.

 

https://www.lesstif.com/pages/viewpage.action?pageId=14745703

 

curl 설치 및 사용법 - HTTP GET/POST, REST API 연계등

curl 은 command line 용 data transfer tool 이다. download/upload 모두 가능하며 HTTP/HTTPS/FTP/LDAP/SCP/TELNET/SMTP/POP3 등 주요한 프로토콜을 지원하며 Linux/Unix 계열 및 Windows 등 주요한 OS 에서 구동되므로

www.lesstif.com

여기에 설명이 아주 잘 돼있다.

 

==============

 

source "$HOME/.sdkman/bin/sdkman-init.sh"

 

여기까지가 sdkman 을 설치하는 과정이고, 버전을 확인하고 싶으면 sdk version 을 입력 해본다.

 

sdk install springboot 

이렇게 하면 스프링 부트 CLI 설치가 진행된다.

 

spring --version 으로 spring CLI 버전을 확인해 보자.

 

 

홈브루 라는 유닉스라이크 운영체제 옵션이 있다. 본래는 macOS 사용자가 유닉스/리눅스 세상에서 빠진 도구를 설치하는 용도로 개발됐다.

홈브루로도 설치할 수 있으나, 일단 넘어가자. sdkman 으로 설치 했으니까.

 

 

스프링 부트 CLI 는 단순히 앱 실행뿐만 아니라 필요한 폴더 구조를 초기화하고 자동 생성하는 용도로도 쓰인다.

 

아주 최소한의 기본 프로젝트를 만드려면 다음과 같이 입력하면 된다.

 

spring init --build gradle myapp

 

--build gradle 옵션을 빼면 기본적으로 메이븐 프로젝트를 만든다.

 

spring init 명령어를 기억하자.

 

 

--여기까지 다시 복습 해보자.

SpringApplication은 스프링 앱을 간편히 초기화하는 특별한 클래스이다.

curl 은 command line용 data transfer tool 이다.

spring init 으로 스프링 부트 프로젝트의 뼈대를 생성할 수 있다.

---

 


컴파일, 테스트 ,빌드 작업에 쓰는 메이븐과 그레이들은 스프링 부트에서도 쓸 수 있다.

-메이븐과 그레이들은 컴파일, 테스트, 빌드 작업에 쓰인다.

 

메이븐을 기준으로, 스프링 부트 앱에 꼭 들어가는 폼 파일을 확인해 보자.

 

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>wp-homepage</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>wp-homepage</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

 

위에서부터 내려가보자.

parent 안에 spring-boot-starter-parent 부분을 보자.

아티팩트를 선언했는데, 앱 실행에 필요한 라이브러리는 모두 여기에 담겨있다.

스프링 부트 시동에 꼭 필요한 스프링 프레임워크(spring-core) , 스프링 테스트 (spring-test) 는 모두 여기에 있다.

이 parent 상위 폼은 반드시 필요하다.

 

생략된 그 아래 부분인,

스타터 폼 섹션에는 실제로 갖다 쓸 스프링 부트 기능에 해당하는 의존체가 나온다.

 

스타터 폼은 앱에 필요한 의존체를 저눕 알아서 가져오므로 딱 하나만 넣는다. 가령 웹 애플리케이션 프로젝트면 spring-boot-starter-web 하나만 있으면 된다.

 

자 이제 spring-init 명령어는 잠시 잊고,

외부 도구에 눈을 돌려보자.

 

스프링 이니셜라이저라는 놈이 있는데, 피보탈 웹 서비스가 운영하는 사이트에 가면 자세한 정보를 얻을 수 있다.

 

원하는대로 골라 담아, 전체 폴더 구조가 고스란히 압축된 zip 파일을 다운받을 수 있게 해준다.

 

이 스피링 이니셜라이저를 curl 로 실행할 수도 있다.

 

세 번 째로는 스프링 도구모음(STS) 를 이용하는 방법이 있다.

STS는 Spring Tool Suite, 스프링 도구 모음의 약자다.

 

이클립스를 쓸 경우 해당 STS 플러그인을 설치하거나 해서 사용할 수 있다.

 

정리---

1.  spring CLI 를 설치한 후, spring-init 

2. 스프링 이니셜라이저를 사용하기

3. STS 플러그인을 설치하고 활용

 

등등 여러가지 방법으로 프로젝트를 생성할 수 있다.

 

intelliJ 같은 걸 설치하면 기본적으로 3번이 설치돼있는 경우가 많다.

 

3번을 이용해서 바로, 처음부터 프로젝트를 진행해보도록 하자.

 

 

 

블로그 이미지

맛간망고소바

,

쓸 데 없이 오래 걸렸다. 오래 걸린 이유가 있다.

IntelliJ, JetBrain은 나에게 똥을 줬다. IntelliJ Education 은 아주 쓰레기였다. 툭하면 강제 종료를 유발했다.

 

결국 껐다 켰다를 반복하며 학습을 계속하다, 종래엔 프로그램이 아예 작동하지 않았다.

프로그램을 끄고 다시 켜도 그 지경이었다. 캐쉬도 삭제해보고, 프로그램을 삭제하고 다시 설치해보기도 했다.

 

결과는 똑같았다. 이 개똥같은 놈.

 

그래서 공부 방법을 바꿨다. 차라리 책을 샀으면 편하기라도 했을텐데...

 

일단 1차 공부는, HyperSkill 에서 하기로 했다.

 

아름답지 않은가? 일단 모두 끝냈다.

간단히 정리도 했다.

 

자바를 주로 쓰다보니 자바를 중심으로 차이점을 찾게 되더라. 일단 재미는 있었다.

문제는 좀 쓸 데없이 귀찮았다. 굳이 이렇게까지 해야 하나 싶었다. (차라리 책을 볼걸)

 

 

변수

 

var 는 변하는, val 는 변하지 않는 변수를 선언할 때 쓰인다.

코틀린은 타입을 중요하게 생각하는 언어다. 타입 추론을 아주 잘 해주는 언어이기도 하다.

 

type

Bit width

Double

64

Float

32

Long

64

Int

32

Short

16

Byte

8

 

대충 사이즈는 위와 같다.

 

 

"Kotlin 에서 Character 는 Number 가 아니다."

자바는 primitive type, reference type 을 두고 있다. 그리고 확실히 구분해서 사용해야 한다.

하지만 코틀린은 그냥 모두 object로 바라볼 뿐이다.

 

 

String 

자바와 상당히 유사하다.

 

  • .first() returns the first symbol of the string.
  • .isEmpty() returns true, if the string has no symbols, otherwise, it returns false.
  • .drop(n) removes n first symbols from the string and returns the resulting string.
  • .reversed() returns the reversed string.
  • .toInt() converts the string to an integer and returns it.

등의 함수들을 제공한다.

자바는 charAt 등을 이용해 String 의 일부 '문자' 값을 읽을 수 있다.

 

코틀린의 String 에선 그냥 배열의 원소에 접근하듯이 접근하면 된다.

 

 

Val greeting = “Hello”

println(greeting.first())   // 'H'

println(greeting.last())    // 'o'

println(greeting.lastIndex) // 4

 

이런 것들도 기억 해두자.

 

코틀린에서 문자열과 숫자는 "불변" 이다. 그 중 문자열 같은 경우..

 

 

val str = "string"

str[3] = 'o' // an error here!

 

var str = "string"

str = "strong”//It’s ok

 

이런 특징을 갖고 있다.

 

중간 값만 못 바꾼다. 그냥 새로운 값을 만들어서 넣어야 한다. 이것이 바로 불변이란 것이다.

 

 

문자열 템플릿

 

  1. Use string templates to insert values of variables into a string like "this string has$value".
  2. If you would like to get an arbitrary expression - use curly braces like "The length is${str.length}".

이렇게 쓸 수 있다.

 

 

Range ( 범위 )

Kotlin 에서는

 

val within = a <= c && c <= b

요런 상황을 대신할

Ranges 라는 개념(?) 제공한다.

 

val within = c in a..b

 

이렇게, 그대로범위 지정하 ㄹ수 있다.

A~b 까지 모두 포함이다. 기억하자. 

 

val withinExclRight = c in a..b - 1 // a <= c && c < b

이렇게 하면 (b -1) 오른쪽 끝은 포함시키지 않는 범위를 만든다.

val range = 1..5 - 2

이렇게, 빼는 숫자를 늘릴수도 있다. 이러면 어찌되는가? 1..3 된다.

 

Not in 수도 있다.

 

val notWithin = 100 !in 10..99 // true

이렇게.

 

심지어 range integer에서만 있는게 아니라, character, String 에서도 있다.

 

println('b' in 'a'..'c') // true

println('k' in 'a'..'e') // false

 

println("hello" in "he".."hi") // true

println("abc" in "aab".."aac") // false

 

range 라는 놈은 어떤 object라고 생각하는게 편하다. 말이냐 하면,

Val range = a..b 

이런 식으로 range 객체를 변수에 할당할 수도 있다.

이렇게 하면

9 in range

이렇게 수도 있다.

 

 

 

Array ( 배열 )

특별한건 딱히 없다.

 

Kotlin provides many types for representing arrays: IntArray, LongArray, DoubleArray, FloatArray, CharArray, ShortArray, ByteArray, BooleanArray. Each array store elements of the corresponding type (Int, Long, Double and so on). Note, there is no StringArray.

 

다만 위와 같이,

타입별로 만들수 있는 Array 있다. String 제외한 기본 타입들은 전부 있다고 보면 된다.

 

 

val numbers = intArrayOf(1,2,3)

println(numbers.joinToString())

 

만약 Array 크기를 지정해서 만들고 싶다면?

 

val numbers = IntArray(5) 

이런식으로 하면 된다. 가장 문자가 대문자가 됐다.

요런 식으로 쓰면 된다.

 

println(array.first()) // "abc"

println(array.last()) // "efg"

println(array.lastIndex) // 2

 

위와 같은 편의 함수도 제공한다. ( String 과 유사해 보인다 )

 

값을 비교할 때는 == and !=  쓰면 안된다. (당연히)

 

만약 값이 같은지 비교하고 싶다면

 

println(numbers1.contentEquals(numbers2)) // true

println(numbers1.contentEquals(numbers3)) // false

 

 

 

Character ( 문자 )

 

코틀린은 문자를 숫자로 보진 않지만, 일관성은 어따 팔아먹었는지 ++ 같은건 또 지원한다. 이게 뭔 소리냐면.

 

val ch1 = ‘b’

ch1 + 1 // ‘c’

 

이게 된다는 말이다.

 

보통 문자는 ' ' 이렇게 따옴표 안에 한 글자 들어가는데...

 

val ch = '\u0040' // it represents '@'

println(ch) // @

 

요런 식으로 \u 를 붙여서 표현할 수도 있다. 헥사데시말 코드 로 나타내는 건데, 잘 모르겠다.

 

 

코틀린의 문자는 많은 멤버 함수를 갖고 있다.

 

 

  • isDigit() returns true if the given character represents a digit ('1''2', etc); otherwise, false;
  • isLetter() returns true if the given character represents a letter ('a''B''m', etc); otherwise, false;
  • isLetterOrDigit() returns true if the given character represents a letter or a digit; otherwise, false;
  • isWhitespace() returns true if the given character represents a whitespace (' ' or '\t'or '\n'); otherwise, false;
  • isUpperCase() returns true if the given character is an uppercase character; otherwise, false;
  • isLowerCase() returns true if the given character is a lowercase character; otherwise, false;
  • toUpperCase() returns the uppercase form of the given character;
  • toLowerCase() returns the lowercase form of the given character.

척봐도 쓸만해 보이는게 많다.

 

함수 (Function)

 

코틀린에서는 모든 함수는 "반환값" 을 갖는다. 정확히는 "반환 하는 것" 이 있다. void 같이, 아무것도 반환하지 않는다면??

 

val result = println("text")

println(result) // kotlin.Unit

This result is a special value called Unit, which means "no result".

 

항상 뭔가 뱉는다. 일단 아무것도 뱉는 같아도 kotlin.Unit 뱉음.

 

함수의 형식은 다음과 같다.

 

 

 

fun functionName(p1: Type1, p2: Type2, ...): ReturnType {

    // body

    return result

}

 

만약 한 줄로 표현할 수 있다면, curly braces 즉 중괄호는 생략 가능하다.

 

 

fun sayHello(): Unit = println("Hello")

 

Specifying the return type is optional, it can be inferred automatically:

fun sum(a: Int, b: Int) = a + b // Int

 

반환형은 안 써도 된다. (충분히 암시적이라면)

 

아, 함수의 argument 에 "기본 값" 을 설정할 수 있다.

 

default arguments allow programmers to create optional or special-case behaviour for functions.

 

fun findMax(n1: Int, n2: Int, absolute: Boolean = false): Int {

    val v1: Int

    val v2: Int

 

    if (absolute) {

        v1 = Math.abs(n1)

        v2 = Math.abs(n2)

    } else {

        v1 = n1

        v2 = n2

    }

 

    return if (v1 > v2) n1 else n2

}

 

 

 

 

val amount = calcEndDayAmount(ticketPrice = 10, soldTickets = 500, startAmount = 1000) 

 

이렇게 아규먼트 이름을 지정해서 수도 있고

 

이름 붙인거랑 붙인거랑 섞어서도 있는데

 

calcEndDayAmount(1000, ticketPrice = 10, soldTickets = 500) // 6000

 

섞어서 때는, 일단 이름으로 지정한 부터는 positional argument 없음.

 

 

 

Function 또한 일정한 타입으로 볼 수 있는데... ex) 3 -> Int , 3L -> Long

 

 

fun sum(a: Int, b: Int): Int = a + b

 

sum has a type of (Int, Int) -> Int.

 

fun sayHello() {

    println("Hello")

}

printHello has a type of () -> Unit 

 

이런식이다.

 

코틀린은 함수 또한 오브젝트처럼 다룰 수 있다.

 

Var mul = {a: Int, b:Int -> a + b}

이러면 mul 변수가 함수를 가리키는변수.

 

즉, 함수를 가리키는 "변수" 를 지정할 수 있다. 

 

mul 변수의 타입을 "명시적" 으로 표현해 볼까 그럼?

 

val mul: (Int, Int) -> Int = {a: Int, b: Int -> a + b}

 

요런식으로 표현이 된다.

 

그럼 함수를 mul 이라는 변수에 담았을 때는 저런 식으로, "참조" 할 수 있다 치고...

 

fun sum(a: Int, b:Int): Int = a + b

 

이렇게 이름이 부여된 함수를 넘기고 싶다면? 

 

fun sum(a: Int, b: Int): Int = a + b

val mul2 = { a: Int, b: Int -> a * b }

 

val increment = placeArgument(1, ::sum)

val triple = placeArgument(3, mul2)

 

:: 를 함수 이름 앞에 붙여서 넘기면 된다.

 

함수를 리턴하는 함수를 보면서, 함수의 타입에 대한 이해를 마치도록 하자.

 

 

 

fun getRealGrade(x: Double) = x

fun getGradeWithPenalty(x: Double) = x - 1

 

fun getScoringFunction(isCheater: Boolean): (Double) -> Double {

    if (isCheater) {

        return ::getGradeWithPenalty

    }

 

    return ::getRealGrade

}

 

 

이름이 없는 함수

 

이름이 없는 함수는 크게 두 가지로 나뉜다. 

 

하나는 익명, 하나는 람다.

 

  • fun(arguments): ReturnType { body } – this one is commonly called an "anonymous function".
  • { arguments -> body } – this one is commonly called a "lambda expression".

 

 

익명 함수가 이미 이름이 없는 함수인데 이게 뭔 개 소리인가 싶을 것이다. 그런데 어쩔 수없다. 영어 표현과 한국어 표현의 차이다.

영어로 표현하면 function which has no name. 요딴식이겠지.

 

하여간, 이름이 없는 함수는 => 어노니머스 함수 와 람다 함수. 이렇게 둘로 나뉜다.

(헷갈리니까 어노니머스 함수 라고 하자.)

 

fun(a: Int, b: Int): Int {

    return a * b

}

 

{ a: Int, b: Int -> a * b }

 

위의 두 표현은 "같은 표현" 이다.

 

 

val mul1 = fun(a: Int, b: Int): Int {

    return a * b

}

 

val mul2 = { a: Int, b: Int -> a * b }

 

이렇게 하면, 사실상 mul1 과 mul2 는 완전 똑같은 행위를 한다.

 

람다 함수의 경우, 매겨변수가 없을 때는

 

{ body }

 

요런 식으로 몸통만 써줘야 한다. -> 화살표는 반드시 생략한다.

 

람다를 매개변수로 받는 쪽은 여러 함축 단계를 거칠 수 있다. 매우 귀찮으니..

 

 

Lambdas and syntactic sugar

람다와, 시태틱 슈가

 

There are ways to make the code more readable for human without changing the code logic. If such a way is provided by the programming language and relates to syntax, the way is called syntactic sugar.

코드를 짜는 논리를 바꾸지 않으면서도, 코드를 좀 더 읽기 쉽게 하는 여러 방법이 있다. 만약 그런 것이 프로그래밍 언어 차원에서 지원될 때

우리는 이런걸 "syntactic sugar" 라고 부른다.

 

Kotlin promotes Functional Programming so there is syntactic sugar for it.

코틀린은 함수형 프로그래밍을 지향(?) 혹은 지원하며 이를 위한 문법적인 지원이 있다.

Let's recall the following example of passing the function as an argument:

 

fun isNotDot(c: Char): Boolean = c != '.'

val originalText = "I don't know... what to say..."

val textWithoutDots = originalText.filter(::isNotDot)


Rewrite it to pass a lambda:

 

val originalText = "I don't know... what to say..."

val textWithoutDots = originalText.filter({ c: Char -> c != '.' })

 

이런식으로 람다 함수를 쓰면, isNotDot 함수를 미리 선언할 필요가 없어진다.

 

Now we act! First of all, Kotlin infers types of many objects, and here specifying the c type isn't necessary:

originalText.filter({ c -> c != '.' })

 

그런데 솔직히 타입은 필요 없잖아? Char 를 굳이 써 줘야해? 생략하자.

 

Secondly, there are situations when the lambda is passed as the last argument. This is the case. Kotlin provides a way to eliminate these bracket sequences ({ }), allowing to write the lambda outside the parentheses:

originalText.filter() { c -> c != '.' }

 

그런데 함수의 매개변수로 넘어가는 람다가, "마지막 변수" 면, 사실 () 뒤쪽으로 빼내는게 좀더 보기 좋지 않냐? 그러니까 뺄 수 있어.

 

If the parentheses are left empty after this operation, they can be removed:

originalText.filter { c -> c != '.' }

 

아니 빼고 나니까 () 가 조금 거슬리네? 아무 변수도 없는데 저게 남아있잖아. 아 그럼, 아무것도 없는 괄호는 그냥 없앨 수 있게 하자.

 

() 안이 비어있으면 생략 가능함.

 

Finally, when there is a single parameter in a lambda, there is an opportunity to skip it. The parameter is available under the it name. The final version of the code removing dots is the following:

val originalText = "I don't know... what to say..."

val textWithoutDots = originalText.filter { it != '.' }

 

음... 다 좋은데. 저 c 가 마음에 안들어. 아니, 어차피 매개변수가 딱 하나인데... 굳이 c 요런 식으로 하나를 써 줘야해? 매개변수가 하나밖에 없을 때는 그냥 c 같은 건 생략할 수 있게 하는 건 어떨까?

 

람다는, 매개변수 없으면 -> 없어지잖아. 그 하나뿐인 매개변수는 it 으로 표현할 수 있게 하자..

 

 

 

결론:

람다를 바꿔 볼까? 코틀린은 형변환 해주니까, 람다 안의 매개변수 타입을 굳이 지정해 필요 없음.  그러니까 :Char 빼보자.

 

그런데 만약 람다가 마지막 아규먼트로 쓰인다? 그럼 () 오른쪽으로 있음.

그런데 왼쪽에 남은 () 안에 아무런 매개변수도 없잖아? 이걸 생략할 있어.

 

하지만 람다가 c -> c 이렇게 쓰는것도 별로인 같아.

왜냐면 매개변수가 하나밖에 없는데 굳이 -> 써야 할까?

 

이럴 때는 -> 빼고 it 있도록 했어. 짱이지?

 

 

.... 아주 그냥;;

 

Lambda 마지막 줄은 자동 return 이야. 마지막 줄에서 뱉는 값이 자동으로 return 된다는 말이지.

 

그런데 중간에 값을 뱉고 싶을수도 있잖아?

 

val textWithoutSmallDigits = originalText.filter {

    if (!it.isDigit()) {

        return@filter true

    }

        

    it.toString().toInt() >= 5

}

 

 

이런 식으로 있어

중간에 return 넣고, @ 마킹해줘야해

보통 @ 에다가 붙이는 이름은, 자기를 콜하는 함수 이름이야. (자기를 매개변수로 받는 )

 

lambda 물론 closure 개념을 갖고 있어.

 

클로저는 나중에 따로 정리하도록 하자.

 

일단 간단히..

 

클로저란?

MDN에서는 클로저를 다음과 같이 정의하고 있다.

클로저는 독립적인 (자유) 변수를 가리키는 함수이다. 또는, 클로저 안에 정의된 함수는 만들어진 환경을기억한다’.

흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다. 말로 설명하면 설명하기가 복잡하니 우선 코드를 보자.

 

 

function getClosure() {

  var text = 'variable 1';

  return function() {

    return text;

  };

}

 

var closure = getClosure();

console.log(closure()); //

 

 

자유 변수를 가리키는 함수. 클로저 안에 정의된 함수는 만들어진 환경 기억해.

여기서 getClousre 함수가 아니라, 안에서 뱉어지는 function() {return text; }

클로저야. 이 클로저는 말 그대로 "문을 닫는 느낌" 이지. text 배열을 자기 방 안에 넣고, 문을 닫아버리는 거야.

 

--

 

if 와 when

 

코틀린에서는

 

if 문이 , 아니라 표현이다.

 

표현은 반드시 반환값을 갖는 것이며, 문은 반환 값을 갖는다기 보다, 값들에 변화를 일으키는 

것들을 일컫는다.

 

만약 if 표현으로서 값을 반환하는 용도로 쓰인다면 반드시 else 있어야 한다.

 

 

또 switch 대신 when 이 있다.

 

When 또한 Expression 으로 있는데

때는 if 처럼 반드시 else 있어야 .

 

val result = when (op) {

    "+" -> a + b

    "-" -> a - b

    "*" -> a * b

    else -> "Unknown operator"

}

println(result)

 

보면, when(op)  이런 식으로 ( ) 안에 변수가 하나 들어가고

 

op 해당하는 값이 있으면 -> 분기를 타는 방식이다.

 

when (n) {

    0 -> println("n is zero")

    in 1..10 -> println("n is between 1 and 10 (inclusive)")

    in 25..30 -> println("n is between 25 and 30 (inclusive)")

    else -> println("n is outside a range")

}

 

 

만약 when 뒤에 아무런 변수도 없다? 그냥 모두 boolean 조건이 필요함.

 

    when {

        n == 0 -> println("n is zero")

        n in 100..200 -> println("n is between 100 and 200")

        n > 300 -> println("n is greater than 300")

        n < 0 -> println("n is negative")

        // else-branch is optional here

    }

이런 식으로...

 

 

Switch 다르게, 번에 분기만 있다. (여러 expression 실행 불가)

 

when(str) {

   “Hello”,”world” -> {

 

   }

}

이런 식으로는 쓸 수 있다. 이렇게 한 분기.

 

반복문

 

repeat(n) 이라는 함수가 있다. Argument int 값을 받아 n 실행한다.

 

While 문은 여타 while 문과 같음.

 

 scanner.hasNext() for strings and scanner.hasNextInt()

 

scanner hasNext()  를 활용할 수 있다.

 

라벨 개념

 

 

loop@ for (i in 1..3) { 

    for (j in 1..3) {

        println("i = $i, j = $j")   

        if (j == 3) break@loop  

    }  

}  

 

귀찮으니 간단히 정리하고 넘어가겠다.

 

val fileName = "src/reading.txt"

val lines = File(fileName).readText()

print(lines)

이렇게 가져오면, 파일의 모든 값을 한번에 가져옴.

 

메모리 제한은 2GB 정도다.

 

val fileName = "src/reading.txt"

val lines = File(fileName).readLines() 

for (line in lines){

    println(line)

} 

 

readLine 도 2GB. 사실상 전부 불러오고, List<String> 안에 한 줄씩 넣어놨을 뿐임.

 

readBytes() – may be helpful if you need to store file contents as an array of bytes:

이 것도, 결국 readText() 를 구현하기 위해 사용하는 세부 함수라서 2GB 메모리 제한임.

 

그 이상의 파일은 다음과 같은 방법을 사용해보라.

 

File(fileName).forEachLine { print(it) }

 

일단 넘어가자. 파일은 나중에 쓸 일이 있을 때 한번 더 훑어봐도 늦지 않는다.

 

 

 

클래스

 

Properties 반드시 초기화 돼야 .

 

Also, a property has a strict type. The type can be any. It can be a standard type like a number or a string or a custom type. So a property type can be your own class and even the same class where the property is declared.

 

 

 

 

Constructors are class members that initialize a new object of the class.

 

class Size {

    var width: Int = 0

    var height: Int = 0

 

    constructor(_width: Int, _height: Int) {

        width = _width

        height = _height

    }

}

 

이런 식의 생성자를 “Secondary constructor” 라고 한다. 차순위 생성자?

대체 생성자?

 

 

Omitting default values

Classes in Koltin have useful feature: you don't have to declare a default property value if the value is assigned in the constructor of the class:

 

 

 

 

Primary Constructor

생성자

존나 코틀린의 새로운 문법 하나인데

 

Class Size constructor(width: Int, height: Int) {}

 

요런 식으로 constructor 선언하는거임.

 

class Size constructor(width: Int, height: Int) {

    val width: Int = width

    val height: Int = height

    val area: Int = width * height

}

 

Primary constructor constructor 키워드는 생략이 되며,

심지어 primary constructor 에서는 변수 선언도 가능하다.

 

class Size(val width: Int, height: Int) {

    val height: Int = height

    val area: Int = width * height

}

 

위와 같이. Width 없던 변수를 선언 놓는 것이다. 

 

위의 클래스 선언은 사실상 번째 것과 다를게 없다.

 

 

class Size(val width: Int, val height: Int)

 

이런 식으로,  {} 생략하고 아주 간단한 Single line class 만들 수도 있다.

 

  • Any Kotlin class has a constructor to initialize its objects.
  • A constructor is available for invocation under the class name.
  • There are different ways to declare a constructor but a primary constructor is the most concise.
  • If a class has no custom constructors, then there is an implicit default constructor with no arguments and it does nothing.

 

 

모든 코틀린 클래스는 오브젝트를 초기화 하기위한 생성자를 갖고 있다.

컨스트럭트는 클래스 이름 아래에 있을 있다.

커스텀 컨스트럭터가 없다면, 기본 생성자가 암시적으로 존재한다.

 

 

enum class Rainbow(val color: String) {

    RED("Red"),

    ORANGE("Orange"),

    YELLOW("Yellow"),

    GREEN("Green"),

    BLUE("Blue"),

    INDIGO("Indigo"),

    VIOLET("Violet")

}

 

Enum 

 

enum class Rainbow(val color: String, val rgb: String) {

    RED("Red", "#FF0000"),

    ORANGE("Orange", "#FF7F00"),

    YELLOW("Yellow", "#FFFF00"),

    GREEN("Green", "#00FF00"),

    BLUE("Blue", "#0000FF"),

    INDIGO("Indigo", "#4B0082"),

    VIOLET("Violet", "#8B00FF");

 

    fun printFullInfo() {

        println("Color - $color, rgb - $rgb")

    }

}

 

 

Enum 기본 속성들

 

Name -> 그냥 이름 Rainbow.RED -> RED

Ordinal -> enum 순서?  Rainbow.GREEN.ordinal -> 3

Values ()-> enum instances 들을 반환.

 

fun isRainbow(color: String) : Boolean {

    for (enum in Rainbow.values()) {

        if (color.toUpperCase() == enum.name) return true

    }

    return false

}

 

반복문은 위와 같이 enum .

 

 valueOf() - returns an instance of Enum by its name with String type and case sensitivity

 

 

If you want to extend your Enum but with static context, then you need to wrap your method with companion object keywords, let's modify our Rainbow in order to find an instance by RGB parameter:

 

Companion object 여기서 처음 등장한다.

 

Static 으로 class member 추가할 사용한다.

 

enum class Rainbow(val color: String, val rgb: String) {

    RED("Red", "#FF0000"),

    ORANGE("Orange", "#FF7F00"),

    YELLOW("Yellow", "#FFFF00"),

    GREEN("Green", "#00FF00"),

    BLUE("Blue", "#0000FF"),

    INDIGO("Indigo", "#4B0082"),

    VIOLET("Violet", "#8B00FF"),

    NULL("", "");

 

    companion object {

        fun findByRgb(rgb: String): Rainbow {

            for (enum in Rainbow.values()) {

                if (rgb == enum.rgb) return enum

            }

            return NULL

        }

    }

 

    fun printFullInfo() {

        println("Color - $color, rgb - $rgb")

    }

}

 

이렇게 쓴다.

 

Member function 

 

자바에서는 클래스 안에 있는 함수를메서드라고 표현하곤 하는데,

여기서는 Member function 이라고 쓴다.

 

코틀린은 클래스에 function 확장 선언하는 기능을 제공한다.

 

이를 우린확장 함수” Extension Function 이라고 부른다.

 

fun String.repeated(): String = this + this

 

Fun 붙이고, “클래스”.”함수이름 하고내용을 선언하면 .

 

"ha".repeated() => “haha”

 

 

  • The syntax to define an extension function is like defining a top-level function. Just write the name of a class which you would like to extend and a dot before the function name. The class to be extended is called a receiver type.

, “클래스리시버 타입이라고 칭한다.

This 키워드를 사용하면, 확장 함수를 사용하는인스턴스 참조할 있다.

 

So the issue is solved: Kotlin developers are able to add any functions to any classes they want.

 

확장함수는 나름 안정성을 확보하기 위한 규칙을 갖고 있다.

일단, 확장함수 끼리는 중복이 안되며 (컴파일이 실패함), 이미 멤버 함수가 있을 , 확장함수가 이를 오버라이딩 없다.

 

 

fun product.takeFunction(productName: String): Product {

this.~~~()

this.~~~()

  return ~~~

}

 

 

 

 

블로그 이미지

맛간망고소바

,

이제보니 IntelliJ Edu 에 Atomi Kotlin 이란 것이 있었다.

내가 코틀린을 모르는 상태로 어찌저찌 docs를 보면서 문제를 풀고, 개념을 잡고 있었는데...

 

지금 보니 조금 멍청한 선택이었다.

 

애초에 기본 문법을 위한 코스가 따로 있었다.

 

Atomi Kotlin 으로 다시 시작하도록 하겠다.

 

-------------

 

Atomi Kotlin 으로 학습을 시작하기 전에, 일단 Kotlin docs를 보면서 의문을 가졌던

 

wildCard , PECS 등등에 대해 대충 정리하고 넘어가겠다.

 

일단 PECS

는 Produce Extends, Consumer Super 를 의미한다.

 

이 때 이 PECS 는 파라미터로 넘어가는 놈! 의 역할을 의미한다. (함수 안에서 이 파라미터가 어떤 역할인지를 생각 해보자.)

 

만약 파라미터로 넘어가는 "이 놈" 이 값을 뱉어내는 놈이다. -> 값을 생산하는 놈이다.

만약 파라미터로 넘어가는 "이 놈" 은 값을 뱉지는 않고, 되려 가져가는 놈이다. -> 값을 소비만 하는 놈이다.

 

사실 PECS는... 리스트(List)를 파라미터로 넘겼을 때 "리스트 관점에서" 생산자인지 소비자인지를 의미한다. 라고 설명된다.

이 리스트가 "나" 라고 생각 해보자. 내가 마트에 가서 물건을 골라 내 리스트에 막 넣는다. 이 때 나는 당연히 소비자이다.

내가 반대로 장바구니를 들고 가서 막 애들한테 나눠준다. 나는 "나눠주는, 생산자" 이다.

 

"나"는 이 함수 안에서, 생산자이다.

"나"는 이 함수 안에서, 소비자이다. 

 

일단, "잠깐 이 놈은 값을 생산도 하고 소비도 하는데?" 라고 할 때는 이 PECS 에서 일단 빼 놓고 얘기하자.

(상관은 없다. 하지만 이 것이 와일드카드랑 섞이기 시작하면 혼란이 찾아온다. 나처럼)

 

간단히 생각하자.

 

만약 값을 생산하는 놈은, 함수에 전달되서, "값을 뱉어내는 놈" 이다.

 

즉 T extends Animal 이라고 할 때,

T 는 반드시 Animal 을 상속하고 있는 놈이다. 따라서 Animal class에 있는 메서드와 필드를 기대할 수 있다.

즉 함수에서 이 T 타입의 파라미터를 사용할 때 아무런 문제가 없다.

 

반대로 이 T 에다가 값을 넣어보려 해보자. Animal 중 뭔지 알 수는 있나? 함수는 정확한 타입을 맞출 수 없다.

따라서 이 T 는 함수에 값을 제공하면 제공했지, 함수에서 값을 받아들일 놈은 아니란 거다. 

 

따라서 요놈은 "생성자" Producer이며, 파라미터로 넘겨질 때는 제네릭 사용시 부모 클래스를 extends 하면서 들어가야 한다.

 

 

반대로 값을 받아들이는 제네릭 T super Cat 이 함수의 파라미터로 넘어간다고 해보자.

요놈은 Cat 의 super 타입인 것이 "보장 돼 있다" 예를 들어 T 에 Animal 이 들어가면,

 

Animal 클래스에 Cat 인스턴스를 넣을 수 있나 없나? 당연히 넣을 수 있다.

 

반대로 이 T 를 함수에서 사용하려 할 때는 골치가 아파진다. 이 T 는 Cat의 super 타입이지만, 어떤 필드를 갖고 있고, 어떤 메서드를 갖고 있는지 알 수가 없다. 이 T 타입의 인스턴스를 사용 할 수 있을까?

 

따라서 이 T 타입의 인스턴스는, 함수에서 값을 가져가는(소비하는) 역할을 하지, 함수에 값을 제공하는 Producer 역할을 하는 것이 아니다.

 

따라서 Consumer 는 Super 다.

 

이런 식으로 PECS를 정리하고 가자...

 


여기서 WildCard ? 를 얹어 보자.

 

WildCard 는 말 그대로, 뭐든지 들어갈 수 있다. 라는 것을 표기하는 것이다. 이 것이 함수의 파라미터에 super, extends 와 같이 쓰일 때는 다른 작용을 한다.

 

첫 째로 WildCard로 넘어온 타입이 뭔지는 "당연히 알 수 없다." 

 

? extends Animal 이라고 해보자.

 

함수의 파라미터로 ? 타입의 인스턴스가 넘어갔다.

 

아니 잠깐만. 이게 T extends Animal 상황이랑 뭐가 다르지? 하는 의문이 들 것이다.

 

다르다.

 

함수에 파라미터로

 

Collection<? extends Animal>

 

요런 식으로 인스턴스를 받아들이면, 이 Collection 인스턴스에는 값을 꺼낼 순 있어도 값을 넣을 순 없다.

 

반대로 

 

Collection<? super Cat>

 

요런 식으로 인스턴스가 함수로 넘어가면, 이 인스턴스에는 값을 넣을 순 있어도, 값을 꺼낼 수는 없다.

 

List<? super Cat> 에다가 List<Animal> 인스턴스를 넣으면,

 

이 List<Animal> 타입의 인스턴스는 "소비자(왕) 이시다." 왕의 값을 감히 빼 볼 수 없다.

 

animalList.get(0) 요런식으로 값을 꺼내려고 하는 순간 바로 에러가 발생한다.

하지만 animalList.add(cat) 은 가능하다. 

 


 

이제 Kotlin 의 in 과 out 으로 넘어 가보자.

 

만약 자바에서 

 

void demo(Source<String> strs) {
  Source<Object> objects = strs;


  // ...
}

는 불가능 하다.

String s = "자바의 경우"

Object object = s 

 

왜냐면 Object는 String의 상위 클래스이기 때문이다.

 

하지만 

Source<Object>는 Sourc<String> 의 상위 타입이 아니다.

이것을 우린 "무공변성" 이라고 한다.

 

Object 는 String 의 상위 타입이니까?

Source<Object> 도 Sourcde<String> 의 상위 타입이겠지?

 

이게 허락되면 우리는 "공변성" 이 있다. (함께 변하는 관계) 라고 한다.

 

반대로 Object는 String의 상위 타입이야.

잉? 그런데 Source<String> 이 Source<Object> 의 상위 타입이야? 

이게 말이 안돼 보이는데 요런 상황이 찾아오면 이것을 "반공변성" (정 반대로 변하는 관계) 라고 한다.

 

 

위의 자바 코드를 가능하게 하려면 이렇게 해야한다.

 

 void demo(Source<? extends Object> strs) {
  Source<?> objects = strs;
  // ...
}

 

하지만 코틀린에서는 in, 과 out 을 사용해서 다음과 같이 사용할 수 있다.

 

interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

여기서 out 의 의미는 T 제네릭을 같는 이 클래스 타입 Source 는 T 타입을 "반환" 할 수는 있지만(out)// T 타입을 파라미터로 받아들일 수는 없다! 를 의미한다.

 

그럼 val objects: Source = strs // 이건 왜 가능한 것인감??

 

Source<Any> 클래스 안에 Source<String> 타입의 "인스턴스" 가 들어가 있다고 생각해보자.

그럼 반환하는 T 는 자동으로 Any 로 나와야 하는데, String 은 Any 하위이기 때문에 Any 타입의 반환에 String 형태의 인스턴스를 반환해도 아무런 문제도 없다.

 

따라서 위와 같은 "공변" 현상이 일어날 수 있는 것이다.

 

 

일단 여기까지만 정리하도록 하겠다. 저녁 약속이 있어 나가봐야... 더 자세한 정리는 다음 기회에... 

블로그 이미지

맛간망고소바

,

2. Conventions

Convention 에는 "관례" 라는 뜻이 있다...

일단 귀찮으니 넘어가자.

 

1) Comparison

첫 번째 문제는 Comparison 이라는 문제다.

여기서 핵심은 

operator 를 overriding 할 수 있다는 개념이다.

 

클래스 내에서 함수를 override 할 때는 operator 키워드를 붙여줄 필요는 없다.

 

Comparable 을 상속해서 , compareTo 함수를 오버라이딩 해주면 끝.

 

이 compareTo 는 operator 에서 <  ==  > 등을 비교하는데 쓰인다.

 

2) In Range

 

이 문제는 보자마자 뭔 개소리지?? 하는 느낌을 주었다.

애초에 자바에 in 같은 키워드가 없기 때문이다. (적어도 난 안쓴다.)

 

쿼리로 따지면 Between 같은 느낌의 연산자이다.

 

"연산자" 이다. < > == 같은 연사자.

 

따라서 연산자 오버라이딩을 하면 된다.

 

Range

A range defines a closed interval in the mathematical sense: it is defined by its two endpoint values which are both included in the range. Ranges are defined for comparable types: having an order, you can define whether an arbitrary instance is in the range between two given instances. The main operation on ranges is contains, which is usually used in the form of in and !in operators.

To create a range for your class, call the rangeTo() function on the range start value and provide the end value as an argument. rangeTo() is often called in its operator form ...

 

class DateRange(val start: MyDate, val endInclusive: MyDate){
    operator fun contains(item: MyDate): Boolean =
            item >= start && item <= endInclusive
}

fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
    return date in DateRange(first, last)
}

이 문제는 이런 식으로 풀면 된다.

 

DateRange 라는 클래스를 만들고, 이 클래스의 main constructor 는 start 와 endinclusive 를 받는다.

그리고 자기가 갖고 있는 start와 endEnclusive 로 넘어오는 MyDate instance 가 범위 안에 속하는지 아닌지를 반환하는 contains 함수를 정의 해준다. 

 

여기서 알 수 있는 것은, "operator" 키워드를 사용해서, 타 클래스의 연산에 관여할 수 있는 클래스를 만들 수 있다는 것이다.

또한 MyDate는 이 전 문제 (1번) 에서 재 정의한 compareTo 함수를 갖고 있다는 것을 명심하자.

 

1) compareTo  로 대소를 배교할 수 있는 클래스(MyDate) 를 만들게 됨.

2) 이제 대소를 비교할 수 있으니 contains 함수도 만들 수 있음. in 연산자를 오버라이딩 한 클래스가 대신 이것을 처리 해 줌.

 

 

3) RangeTo

 

확장 함수로 연산자 오버로딩을 할 수 있다.

이 전 2번 문제에서는 

date in DateRange(first,last) ... 방식으로 범위 안에 속하는지를 검사 했지만.

 

date in first..last 형식으로 이를 바꾸는 것이다.

 

date 도 first 도 last 도 모두 MyDate 클래스의 인스턴스이다.

 

.. 을 사용하기 위해서는 rangeTo operator 를 만들어 줘야 한다.

 

operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)

class DateRange(override val start: MyDate, override val endInclusive: MyDate): ClosedRange<MyDate>

 

위와 같이, operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)

로 만들면 된다.

 

2단원 부터 갑자기 난이도가 올라가는 면이 있는데, 대부분이 오버라이딩에 관련된 문제인데 함수의 원형을 알기 힘들기 때문이다.

 

일단 문서(docs)를 타고 올라가서 rangeTo 의 틀을 찾아보자.

 

 

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/index.html

 

kotlin.ranges - Kotlin Programming Language

 

kotlinlang.org

여기에 가서 rangeTo 를 찾아보자.

 

ClosedRange 타입의 인스턴스를 반환해야 함을 알 수 있다.

 

DateRange 클래스는 현재 ClosedRange<MyDate> 를 상속하고 있다.

따라서 DateRange 인스턴스를 만들어서 반환하면 되는구나? 라는 것을 생각할 수 있다.

 

여기서 다시 정리해보자.

 

1. compareTo  로 MyDate 클래스의 인스턴스는 대소비교가 가능해짐

2. DateRange 라는 클래스로 contains 를 오버로딩 하면 in 연산자를 사용할 수 있음.

3. rangeTo 를 만들면 first..last 형식으로 쓸 수 있음. rangeTo 의 반환형은 뭐다? -> ClosedRange 다.

 

그럼 쉽게 예상할 수 있을 것이다. ClosedRange 를 상속 받으면 무엇을 오버라이딩 해줘야 할까? -> contains다.

결국 우리가 2번에서 만든 contains 함수를 갖는, DateRange instance를 반환하게 하는 것이 rangeTo 다.

 

따라서..

 

date in DateRange(first,last)  = date in first..last

 

first..last = first.ragneTo(last) = DateRange(first, last)

 

4) For loop

in 은 단순히 대소 비교를 통해, 범위 안에 값이 들어가는지 확인만 하는 것은 아니다. (아닌가?_

 

for 문 안에서도 사용될 수 있다.

 

예를 들면

 

for(date in firstDate..secondDate) {

 

}

와 같이 사용된다.

 

하지만 여기서는 같은 in 키워드여도 다른 것이 필요하다. 단순한 ClosedRange 가 아닌, Iterable 을 상속해야 하며.

Iterabe 인터페이스를 구현하면, Iterator를 반환해야 하고.

 

이 Iterator를 반환하기 위해 새로운 클래스를 만들어야 한다.

 

그냥 Iterator를 anonymous object로 전달하자 무한 루프에 빠져 버렸다. (다른 요인일 수도 있다.)

anonymous object로 만들지 말고, 클래스를 하나 만들고, 인스턴스로 Iterator를 하나 만들어서 뱉어주니 잘 작동했다.

 

 

class DateRange(val start: MyDate, val end: MyDate): Iterable<MyDate>{
    override fun iterator(): Iterator<MyDate> = DateIterator(this)
}

class DateIterator(val dateRange:DateRange) : Iterator<MyDate> {
    var current: MyDate = dateRange.start
    override fun next(): MyDate {
        val result = current
        current = current.nextDay()
        return result
    }
    override fun hasNext(): Boolean = current <= dateRange.end
}

fun iterateOverDateRange(firstDate: MyDate, secondDate: MyDate, handler: (MyDate) -> Unit) {
    for (date in firstDate..secondDate) {
        handler(date)
    }
}

 

이런 식으로 작서 ㅇ되는데...

 

firstDate..secondDate 를 보면 알 수 있듯이, rangeTo 연산자를 오버라이딩 했다.

rangeTo 함수는 이전 3번처럼 DateRange 인스턴스를 반환하다. DateRange 는 Iterable 을 구현한 클래스이다.

 

Iterator 는 next 와 hasNext 함수를 상속해줘야 한다. 살짝 IntelliJ의 자동 완성 기능의 도움을 받으면 쉽게 오버라이딩 할 함수의 틀을 만들 수 있다.

 

5) Operator overloading

 

오버로딩과 오버라이딩의 차이는 잘 알 것이다.

 

보통 부모의 함수를 상속받아, 자기 껄로 대체 하는것을 오버라이딩이라고 하고.

함수 이름은 같은데 매개변수가 다른 여러 함수를 만드는 것을 오버로딩이라고 한다.

 

오버라이딩은 런타임 시에, 오버로딩은 컴파일 시에 정의 된다... 

 

간략히 여기 까지만 설명 하고.

 

연산자 오버라이딩에서 왜 갑자기 연산자 오버로딩이 제목이 된 걸까? 이를 활용하는 것이 이번 문제의 핵심이자. 정말 재밌는 문제다.

 

난 못 풀었다.

 

6) Destructing declarations.

 

자바스크립트에 "구조 분해 할당" 이란 것이 있다.

 

그거랑 비슷 한건데,

fun isLeapDay(date: MyDate): Boolean {

val (year, month, dayOfMonth) = date

// 29 February of a leap year
return year % 4 == 0 && month == 2 && dayOfMonth == 29
}

 

이렇게 있을 때 MyDate 의 필드 값들이 year, month, dayOfMonth 에 분해돼 들어가는 느낌을 주는... 

 

이것 또한 연산자로 지정해 줘야 한단다...

 

다음과 같다.

 

class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) {
    operator fun component1(): Int {
        return year
    }
    operator fun component2(): Int {
        return month
    }
    operator fun component3(): Int {
        return dayOfMonth
    }
}

componentN() 을 정의해주면 된다.

N 에 1~N 까지으 숫자가 들어간다.

 

6) Invokable

 

별 거 없는 문제인데 한 번 기억해두면 좋을 요소가 하나는 있다.

 

class Invokable {
    var numberOfInvocations: Int = 0
        private set
    operator fun invoke(): Invokable {
        numberOfInvocations++
        return this
    }
}

fun invokeTwice(invokable: Invokable) = invokable()()

짜잔.

 

invoke 라는 연산자를 오버라이딩하면..

인스턴스에다가 () 를 붙여서 함수처럼 쓸 수 있다...  위에 invokeTwice 는 말 그대로 invoke 두번 한거다.

 

여기서 private set 부분을 한번 보고 넘어가자.

 

 

Kotlin 에서는 클래스 안에 var a = 3 이런 식으로 "필드" 를 선언해 놓으면 자동으로

get 과 set 함수가 생긴다.

 

자바에서는 아마 @Getter @Setter 등을 붙이거나. (lombok 사용 시에) 직접 만들어야 했을 것이다.

 

만약 상수 선언을 의미하는 val 을 쓴다면? setter 가 생기지 않는다.

 

위에서 필드 선언 부분 아래에서 private set 부분을 확인할 수 있다.

set 함수를 private으로 만든 것이다. var 로 선언했기 때문에 값이 변할 수는 있지만, 이 값을 외부에서 직접 특정 값으로 수정하지는 못하게 한 것이다.

 

이 부분만 집고 넘어가자.

 

2단원은 이렇게 마친다.

 

블로그 이미지

맛간망고소바

,

1. Introduction

1.1 Hello, World!

 

코틀린에서는 function을 다음과 같이 선언할 수 있다.

fun start(): String = "OK"

String 을 반환형으로 사용하는, 파라미터를 받지 않는 start 함수다.

 

https://kotlinlang.org/docs/reference/basic-syntax.html#defining-functions

 

Basic Syntax - Kotlin Programming Language

 

kotlinlang.org

링크가 걸려있다. function 을 여러 방법으로 선언할 수 있다.

 

1.2 Java to Kotlin conversion

자바 코드를 자동으로 코틀린 코드로 바꿔주는 것을 보여주는 예제다.

그냥 JavaCode를 복사해서 붙여넣으면 끝이다. Java to Kotlin Converter의 힘을 감상하기만 하면 끝.

 

1.3 Named arguments

함수를 직접 써보는 예제다. 코틀린은 함수에 파라미터를 넘길 때, 파라미터 이름을 지정해서 넘길 수 있다.

이름을 지정 안하면 순서대로 넘어간다.

 

fun joinOptions(options: Collection<String>) = options.joinToString(", ", "[", "]")

 

코틀린의 Collection은 기본적으로 자바와 같다고 보면 된다. 여기서 더 편리한 기능 확장이 추가돼 있다.

콜렉션이 넘어오면, 인스턴스로 함수를 호출한다.  위와 같이 호출 할 수도 있고,

 

fun joinOptions(options: Collection<String>) = options.joinToString(separator = ", ",
prefix = "[",
postfix = "]")

 

이런 식으로도 호출 할 수 있다.

 

또한 함수를 선언 시에 Default Argument 를 사용할 수 있다.

 

fun 함수이름(a: Int = 1, b: Int = 2) = a + b

이런 식으로 해 놓으면 파라미터에 아무것도 넘어가지 않을 때, 기본 값을 사용한다.

 

"코틀린은 함수 선언시에 기본 값을 지정할 수 있다. 또한 함수 호출 시에 파라미터 이름과 함께 넘길 수 있다."

 

1.4 Default Arguemnts

위에 설명한, 기본 값 지정에 대한 문제이다. 나처럼 풀면 이렇게 된다.

 

fun foo(name: String, number: Int, toUpperCase: Boolean) =
(if (toUpperCase) name.toUpperCase() else name) + number


fun foo(name: String, number: Int) = foo(name, number, false)

fun foo(name: String) = foo(name, 42)

fun foo(name: String, toUpperCase: Boolean) = foo(name, 42, toUpperCase)

 

fun useFoo() = listOf(
foo("a"),
foo("b", number = 1),
foo("c", toUpperCase = true),
foo(name = "d", number = 2, toUpperCase = true)
)

하지만 코틀린은 "기본 값을 지정" 할 수 있다.

다음과 같이 간단히 작성 할 수 있다.

 

fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false) =
(if (toUpperCase) name.toUpperCase() else name) + number


fun useFoo() = listOf(
foo("a"),
foo("b", number = 1),
foo("c", toUpperCase = true),
foo(name = "d", number = 2, toUpperCase = true)
)

 

1.5 Lambdas

람다는 자바에서도 8 버전에서부터 지원하는 것으로 알고 있다.

코틀린 또한 당연히 람다를 지원한다.

 

함수를 파라미터로 받는 함수를, Higher order Function이라고 한다.

이런 함수에, 파라미터를 넘겨줄 때 람다를 쓰면 아주 깨끗한 코드를 만들 수 있다. (사람마다 코드의 미적 기준은 다를 수 있다.)

 

아래가 그 예시이다.

 

// Lambdas are code blocks enclosed in curly braces.
items.fold(0, { 
    // When a lambda has parameters, they go first, followed by '->'
    acc: Int, i: Int -> 
        print("acc = $acc, i = $i, ") 
    val result = acc + i
    println("result = $result")
    // The last expression in a lambda is considered the return value:
    result
})

 

 

함수 타입은 다음과 같이 나타낸다.

(R, T) -> R

 

코틀린은 변수를 쓸 때  var a: Int = 1  이런 식으로 쓸 수 있다. 

함수 타입이 (R, T) -> R 과 같은 방식이란 말은

 

var sampleFunction: (Int, Int) -> String = {
a: Int, b: Int -> "the result is $a + $b"
}

print(sampleFunction(1, 2))

결과: the result is 3

 

요런 식으로 쓸 수 있다는 말이다.

 

코틀린은 특이한 문법이 있다.

가장 마지막에 들어가는 "함수 인자" 는 parenthesis 밖으로 뺄 수 있다. (paraenthesis = ( or ) )

 

이게 무슨 개소린가 싶기도 한데...

 

fun higherOrderFunctionDefault(a: Int, b: Int, f: (Int, Int) -> Int): Int {
return f(a, b)
}

var result = higherOrderFunctionDefault(1,2, {a: Int, b: Int -> a + b})

print(result) -> 결과는 3이 나온다.

 

위와 같은 경우에.

 

var result = higherOrderFunctionDefault(1,2, {a: Int, b: Int -> a + b})

이 부분을

 

var result = higherOrderFunctionDefault(1,2){a: Int, b: Int -> a + b}

 

이렇게 쓸 수 있다는 말이다. 심지어 IntelliJ Edu 에서는 뒷 부분을 더 선호한다. ??? 

 

1.6 Strings

이건 그냥 답 봤다. 도저히 못 찾겠다.

String Template 에 관련된 문제인데... Docs 링크가 걸려있긴 하지만, 전혀 몰랐던 Template 적용 방식이 있었다.

자바 때부터 전해내려온 것인지, Kotlin에만 있는 것인지조차 잘 모르겠다. 이 부분은 좀 약하다.

 

1.7 Data Classes

class 앞에 data 를 붙이면, lomobk 의 @Data 와 비슷한 작동을 한다.

 

equals/hashCode, toString

 

함수를 자동으로 만들어 준다.

 

문제 자체는 간단하지만, 여기서 Kotlin의 클래스 개념을 잡고 넘어가야 한다.

 

오른쪽에 있는 링크를 따라가 차분히 읽어보도록 하자.

많은 것들을 다 적을 순 없고 간단히 특이한 점만 정리해 놓겠다.

 

1. 코틀린의 클래스 안의 필드들은, 반드시 초기화 돼야 한다.

2. main counstructor  라는 놈이 있다.

3. 일단 상속되는 놈이 먼저 생성되는건 자바와 똑같다. 따라서 주의할 점도 똑 같다.

4. 클래스 안에서의 문(Statement) 들은 무조건 순서대로 시작된다. 단 constructor는 예외 케이스가 있다.

   main constructor 가 있다면, 클래스 안쪽에 선언하는 놈들은 다 secondary constructor 인데,

   constructor keyword 를 이용해서 생성자를 추가할 수 있다. 이 때 반드시, main constructor을 this(name) 이런 식으로 호출해 줘야한

   다... 

ex)

class Constructors(var a: Int = 3) {
	constructor(i: Int, b: Int): this(i) {
  		println("Constructor")
	}
	init {
  		println("Init block")
	}
}

따라서 위와 같은 경우 init 이 먼저 시작된다. secondary constructor 보다는 init 이 우선시 된다.

main constructor 가 없다? 없을 수는 없다. 사실 암시적으로 갖고 있는 것이다. 마치 자바의 기본 생성자가 있듯이... 만약 main constructor 가 없다면, 암시적으로 호출하고 있다고 생각해야 한다.

 

그렇다면 순서대로라는 것은 무슨 의미인가?

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints ${name}")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

위와 같은 경우 반드시 순서대로 시작된다.  (위의 also 는 kotlin 의 확장 함수 중 하나이다. 자기 자신을 매개변수로 넘기고, 자기 자신을 return 한다.

val person: Person = getPerson().also {
	print(it.name)
	print(it.age)
}

마지막엔 자동으로 자기 자신을 return 한다.

 

5. main constructor 에는 연산을 넣을 수 없다. 따라서 연산을 위해서는 클래스 안에 init 을 사용하도록 해라.

6. class 는 클래스 : 부모클래스() 와 같이 상속. interface  는 클래스: 언터페이스 와 같이 상속한다.

7. 상속 하려면 open 키워드를 붙여야 한다.

 

다른게 무지 많다. 자세히 알아보지도 않고 대충 정리만 해놨다. 무엇보다 1번이 가장 애매한데... 초기화 안하면 컴파일러에서 빽빽돼서 저리 적어 놓았다.

 

1.8 Nullable Type

꽤나 중요한 문제이다.

fun sendMessageToClient(
        client: Client?, message: String?, mailer: Mailer
){
    val email = client?.personalInfo?.email
    if (email != null && message != null) {
        mailer.sendMessage(email, message)
    }
}

위가 정답이다. 깔끔하고 멋있다. kotlin 의 ? 의 힘이다.

자료형에 ? 가 붙어있는 것을 볼 수 있는데, null 이 들어갈 수도 있는 자료형을 나타낸다.

인스턴스에 ? 를 붙인뒤, 인스턴스의 자료 혹은 함수에 접근하려고 하면, null 을 자동으로 체크해준다.

만약 접근하려는 놈이 null 일 경우 그냥 null 을 반환한다. 그래서 마지막에 email != null 을 체크하는 것이다.

 

처음에 if 문을 하나만 써서 해결하라는 문구를 보고 나는 다음과 같이 문제를 해결했다.

fun sendMessageToClient(
        client: Client?, message: String?, mailer: Mailer
){
    if(client != null
            && message != null
            && mailer != null
            && client.personalInfo != null
            && client.personalInfo.email != null)
        mailer.sendMessage(client.personalInfo.email!!, message!!)
}

 굳이 이 흉한 코드를 올려놓은 이유는, !! 또한 키워드 중 하나이기 때문이다. ? 자료형의 변수에, !! 를 뒤에 붙여주면.

"이 값은 null 일리가 없다!" 라는 것을 의미한다. 따라서 String? 자료형의 변수는 String 으로 변해서 파라미터로 전달되는 효과가 있다.

 

1.9 smart casts

 

제목 그대로 "똑똑한 형변환" 을 의미한다. 일단 kotlin 에서는 if 문과 when 문을 사용해서 변수에 값을 할당할 수 있다는 것을 알고 넘어가자.

var a = 3;

var b = if(a == 3) 4 else 5 

이런 식으로 쓸 수 있다는 말이다. (많이 신기함..)

 

위에서는 If문과 When 문이라고 했지만.. Kotlin 에서의 If 는 "Expression" 이다.

이게 뭔 의미냐면, 반환형이 있다는 말이다.

If 표현의 마지막 문장은 자동으로 "반환 값" 이 된다.

 

따라서 Kotlin 에는 ? a : b 표현이 없다. If 로 충분하기 때문이다.

 

when 은 Java에서의 Switch 랑 비슷한데 여러모로 다른 점이 있다. 

fun eval(expr: Expr): Int =
        when (expr) {
            is Num -> expr.value
            is Sum -> eval(expr.left) + eval(expr.right)
            else -> throw IllegalArgumentException("Unknown expression")
        }

위를 보면 알 수 있듯이, when 은 is 로 타입을 확인 할 수 있다. 자바로 따지면 instance of 같은 느낌이다.

왜 "똑똑한 형변환" 일까?

위에서 Expr 이 인터페이스라고 하자. 인터페이스를 구현한 클래스 A : Num , B : Sum 이 있다.

그럼 eval 함수에는 A 도, B 도 들어갈 수 있겠지? 

 

이 때 when 의 is 로 타입을 구분해서, 사용할 수 있다는 말이다.

 

잘 보면 expr.value, expr.left expr.right 라고 저녛 다른 것들을 사용 하는 것을 볼 수 있다.

 

interface 가 when 절의 is 를 통과했을 때, "자동으로" is 에서 비교한 타입으로 형변환 해서 넘어간다.

따라서 is Num 을 통과한 녀석은 Num 자료형으로 자동으로 변경돼서 들어간다.

똑똑하다는 말이다.

 

when의 is 는 타입을 비교할 때 쓴다, 보통은 왼쪽에 "조건식" 이 들어간다.

 

when (num) {

  1 -> "하나"

 2 -> "둘"

else -> "둘 초과"

}

이런 식으로 사용할 수 있다.

 

만약 when 에 아무런 인자도 넘어가지 않는다면, 그냥 "순수한 조건식" 만을 이용해서 사용할 수도 있다.

 

var a = 3

 

when {

  a == 3 -> "a는 3이 맞어"

  else -> "a 는 3이 아니여"

}

이런 식으로 말이다.

물론 위와 같이 쓸 필요는 없다. 위와 같으 2개의 분기로 나눠 질때는 if를 쓰면 된다.

 

3가지 이상의 분기로 나눠질 때 when을 사용하도록 하자.

 

1.9 Extension Function

 

확장 함수를 의미한다. 클래스의 확장 함수를 부여 할 수 있다.

그냥 클래스 안에다가 함수 만드는 것은 확장 함수가 아니다.

 

1.10 Object Expression

코틀린에서는 익명 클래스를 어떻게 사용할 수 있을까?

 

object: 인터페이스{

 override fun 함수이름() {}

}

    Collections.sort(arrayList, object: java.util.Comparator<Int> {
        override fun compare(o1: Int?, o2: Int?): Int {
            if(o1!! > o2!!) return -1 else if(o1 < o2) return 1 else return 0
        }
    })

 

https://kotlinlang.org/docs/reference/object-declarations.html

 

Object Expressions, Object Declarations and Companion Objects - Kotlin Programming Language

 

kotlinlang.org

위의 object 표현식을 활용 하도록 하자.

주의 해야할 점은, 이 anonymous object 의 scope 이다.

anonymous object는 private이나 local 에서만 사용 가능하다.

 

public 으로 노출되면, 이 object는 순식간에 Any? 타입으로 바뀌어 버린다.

(Kotlin 에서의 Any 는 Java에서의 Object와 비슷한 맥락이지만. 절대 같지는 않다.)

 

class C {
    // Private function, so the return type is the anonymous object type
    private fun foo() = object {
        val x: String = "x"
    }

    // Public function, so the return type is Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // Works
        val x2 = publicFoo().x  // ERROR: Unresolved reference 'x'
    }
}

 

1.11 SAM Conversion

 

SAM Conversions

Just like Java 8, Kotlin supports SAM conversions. This means that Kotlin function literals can be automatically converted into implementations of Java interfaces with a single non-default method

 

1.12 Extension On Collection

 

문제 자체는 아주 간단하다. 

 

제목 그대로 Collection을 어떻게 확장해서 사용하고 있는지를 알려준다.

 

차트를 보면 3단원에 제대로 Collection을 다룰 예정인듯 하니 기다리도록 하자.

 

 

 

 


 

이렇게 1단원을 끝냈다.

굳이 정리할 필요는 없겠지만, 기본 문법이니 만큼 적어두는 것이 좋다고 생각했다.

 

이 포스트의 내용 만으로는 완벽히 Kotlin을 이해하기 힘들다.

글을 쓰고 있는 사람부터가 이해하고 쓰는게 아니고, 이해하기 위해, 혹은 이해하고 나서 훗날 기억하기 위해 적고 있기  때문이다.

 

화이팅 하자.

블로그 이미지

맛간망고소바

,

Voice Diary 를 만들기 위해, 안드로이드 어플리케이션을 만들어야 한다.

안드로이드 개발 책을 샀긴 했는데, Kotlin 에 대한 설명이 조금 부족해 보였다.

 

물론 어플리케이션을 만드는데는 충분할 정도로 설명이 돼있긴 했으나, 학습이 목적이니만큼 코틀린 부터 익숙해 지고자 한다.

 

일단 IntelliJ Edu 를 설치했고, Kotlin 부분을 학습 중이다.

살짝 어려운 감이 있는 것이, 일단 Java를 알고 있다는 전제 하에 학습이 진행된다.

 

학습은 문제를 제시하고 대뜸 풀라고 하는 형식인데, 나름 옆에 힌트가 있다.

이게 또 제법 퍼즐 푸는 느낌이 있어 재미있기도 하다.

 

IntelliJ Edu에서 제공하고 있는... 교육용 프로젝트

현재 1단원을 다 풀고 2단원을 풀고 있다.

이런 식으로 공부해 본 적은 없어서 색다른 기분이 든다.

 

Kotlin Docs로 공부하려고 했지만, 내용이 방대하여 살짝 거리감이 느껴졌는데, 이렇게 문제를 풀면서 Docs를 읽으니 체계적으로 정리가 되는 기분이다.

 

문제를 풀려면 반드시 Kotlin을 알아야 한다. 문제를 보고, 이 문제를 풀기 위한 Kotlin Docs를 찾아보고, 답을 적어서 Check를 누르면 된다.

도저히 못 풀 것 같으면 일단 틀린 다음에 Peek Solution 을 보는 것도 나쁘지 않다.

 

처음 써보는 언어라 문법 자체가 생소할 수 있다. 부끄럼 없이 Peek Solution 을 누르자. :)

-계속 누르지만 않으면 된다.-

블로그 이미지

맛간망고소바

,

프로젝트 이름은 아무 생각 없이 정했습니다.

그냥 일기 쓰기 귀찮을 때, 녹음으로 일기 하면 참 편할 것 같다는 생각이 들어 제작하게 됐습니다.

 

그냥 핸드폰 기본 음성 기록 어플 쓰면 되는 거 아니야? 라고 생각할 수 있겠지만... 사람들이 기본 녹음 기능을 일기로써 사용하지 않는 데엔 다 이유가 있습니다.

 

이유는 간단합니다. 핸드폰의 기본 녹음 기능이 "일기 용도" 가 아니라는 인식 때문입니다.

 

아마 제가 만드려는 어플은 분명 이미 개발돼 있을 겁니다. 하지만 상관이 없지요.

이 프로젝트는 제가 생각하기에 간단하고, 재미 있을것 같은 주제를 정한 것입니다.

(간단하지 않을 수 있습니다. 그냥 제 생각입니다.)

 

프로젝트에는 Spring Boot 로 서버를 구축하고, 안드로이드로 프론트를 개발할 예정입니다.

 

사실 안드로드이드 개발은 지금이 처음입니다.

 

스프링으로 서버 개발은 조금 해 보았지만, 이번 기회에 착실히 기본부터 다져가고자 합니다.

혹시라도 이 포스트를 보게 되시는 분이 있다면, 멍청한 방향으로 나아가는 프로젝트를 보고 감탄하실지도 모릅니다.

 

최대한 기본에 충실해 개발 단계를 밟아나갈 예정입니다. 혹시 보시는 분이 있다면 쓴 소리는 달게 받겠습니다만, 

"왜 이딴 프로젝트를 만들고 있지?" 라는 말은 삼가해 주세요...

 

이 프로젝트는 개인 학습용 + 재미용 프로젝트입니다.

티스토리에 글을 남기는 이유는, 혹시라도 제가 중간에 포기할 것을 염려하여 증거를 남기기 위해서입니다.

 

화이팅.

 

블로그 이미지

맛간망고소바

,