스프링 오랜만에 다시 보내 새롭네요. 우연히 접할 기회가 있어서 스프링 부트를 접하게 되었습니다. 기존에 주로 회사에서는 php, ruby on rails, javascript 등 스크립트 언어를 해오다 보니 꽤나 쉽지 않네요. 뭐 이걸로 프로젝트를 한 번 하면 금방 배우겠죠. 루비 같은 경우도 작년까지 전혀 몰랐는데, 프로젝트를 하다보니 점점 알게 되더라구요. 

1. 셋팅

이걸 하면서 gradle이라는 것을 알게 되었는데, 보니까 maven 같은 것인데, 확실히 maven보단 설정 문법이 더욱 깔끔하고 좋네요. maven에서는 xml 지옥이라 알아보기 힘들었는데, gradle은 그냥 스크립트 형식으로 되어있어서 더 알아보기 쉽게 되어있습니다. 

build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.10.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'spring-boot'

sourceCompatibility = 1.5
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.11'
    compile("org.springframework.boot:spring-boot-starter-web:1.2.0.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("mysql:mysql-connector-java:5.1.34")
}

맨 위에 buildscript부분은 jar파일 만들기 위해서 필요한 것 같아요. 그 외에 plugin이 spring-boot가 추가되었는데, bootRun 이런 task 등이 추가 되어있어요.

dependencies는 스프링 부트를 쓰려면 spring-boot-starter-web이 있어야 하고, 데이터연동하는 것을 하기 위해서는 spring-boot-starter-data-jpa가 있어야 하고, mysql을 쓰려면 mysql-connector-java를 추가해야 해요.
jpa에는 하이버네이트가 내장되어 있어요.


2. 메인 클래스

스프링 부트는 예전 개발방식이랑 틀리게 톰캣 받고, web.xml을 셋팅하고, 웹개발을 위한 셋팅이 필요없이 기존 java 실행 방식으로 실행하며 이 실행과 동시에 내장된 톰캣이 작동하여 서버를 만들어버립니다. 아래와 같이 메인클래스를 만들어버리면 그냥 웹서버애플리케이션이 뜹니다.

Application.java

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);

    }
}

보면 예전에 자바처음 배울 때 쓰는 public static void main클래스로 실행만 합니다. @SpringBootApplication어노테이션을 붙이면 최초 기본셋팅으로 톰캣을 띄워서 8080포트로 서버를 만들어 줍니다.


3. 모델 생성

그 전에 접속할 db정보를 입력해야 합니다. 
application.properties

spring.datasource.url=jdbc:mysql://127.0.0.1/sosi?autoReconnect=true&useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database=mysql
spring.jpa.show-sql=true

대략 설정값은 저런데 ddl-auto부분이 create-drop은 서버 재시작 때마다 테이블을 날려버리는 옵션입니다. 처음에 개발할 때에는 매우 편리합니다. 값을 매번 지우지 않고도 서버만 재시작해도 처음부터 다시 개발 해놓은 것을 테스트해볼 수 있으니깐염.

Sosi.java

@Entity public class Sosi { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @Column(nullable = false) private String name; @OneToMany @JoinColumn(name="sosi_id", referencedColumnName="id") private List<Schedule> scheduleList; public Sosi() { } public Sosi(String name) { this.name = name; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Schedule> getScheduleList() { return scheduleList; } public void setScheduleList(List<Schedule> scheduleList) { this.scheduleList = scheduleList; } }


Schedule.java

@Entity
public class Schedule {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne
    @JsonBackReference
    private Sosi sosi;

    @Column
    private String program;

    public Schedule() {
    }

    public Schedule(Sosi sosi, String program) {
        this.sosi = sosi;
        this.program = program;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Sosi getSosi() {
        return sosi;
    }

    public void setSosi(Sosi sosi) {
        this.sosi = sosi;
    }

    public String getProgram() {
        return program;
    }

    public void setProgram(String program) {
        this.program = program;
    }

}

두 개의 모델이 1:N관계 입니다. Sosi가 스케줄을 여러개 가질 수 있는 구조입니다. 여기서 Schedule에는 ManyToOne을 걸었는데, @JsonBackReference도 같이 넣었습니다. 이걸 안 넣으면 서로 계속 참조해서 JSON출력할 때 무한루프에 빠지더라구요-_- 이제 Sosi참조는 json에서 안쓰는 그런 옵션 같습니다.

그리고 예전에는 Dao만들어서 뭔가 하이버네이트세션 가져와서 그걸 통해서 했던 것 같은데, Spring Data JPA에서는 JpaRepository라는 것을 제공하는데, 이것을 통하면 해당 모델에 대해서 CRUD를 제공합니다. 

SosiRepository.java

public interface SosiRepository extends JpaRepository<Sosi, Long>{
}

ScheduleRepository.java

public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
}


4. 컨트롤러 생성

간단하게 소녀시대 정보를 가져오는 컨트롤러와 스케줄 추가 및 가져오는 컨트롤러를 생성합니다.

SosiController.java

@RestController
@RequestMapping("/sosi")
public class SosiController {

    @Autowired
    private SosiRepository sosiRepository;

    @RequestMapping("{sosiId}")
    public Sosi getSosi(@PathVariable Long sosiId) {
        Sosi sosi = sosiRepository.findOne(sosiId);
        return sosi;
    }
}

ScheduleController.java

@RestController
@RequestMapping("/schedule")
public class ScheduleController {

    @Autowired
    private ScheduleRepository scheduleRepository;

    @Autowired
    private SosiRepository sosiRepository;

    @RequestMapping("{scheduleId}")
    public Schedule getSchedule(@PathVariable Long scheduleId) {
        Schedule schedule = scheduleRepository.findOne(scheduleId);
        Sosi sosi = schedule.getSosi();
        return schedule;
    }

    @RequestMapping("add/{sosiId}")
    public Schedule addSchedule(@PathVariable Long sosiId, @RequestParam(value="program") String program) {
        Sosi sosi = sosiRepository.findOne(sosiId);
        Schedule schedule = scheduleRepository.save(new Schedule(sosi, program));

        return schedule;
    }
}

소스 내용을 보면 Repository클래스를 통해 Autowired하면 기본 인터페이스를 해당 모델기반 구현체를 만듭니다. 그 구현체에서 save, findOne 등 함수를 통해 데이터 삽입 및 가져올 수 있습니다.


5. 기본 값 삽입

기본적으로 소녀시대 멤버를 삽입하고 시작할 수 있습니다. 기존 application.properties에 하이버네이트 설정을 재시작하면 꺼지게 해놨기 때문에 기본적으로 앱을 시작할 때 값을 삽입하고 하면 편하게 테스트할 수 있습니다.

Application.java

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
        SosiRepository sosiRepository = context.getBean(SosiRepository.class);

        sosiRepository.save(new Sosi("태연"));
        sosiRepository.save(new Sosi("윤아"));
        sosiRepository.save(new Sosi("수영"));
        sosiRepository.save(new Sosi("효연"));
        sosiRepository.save(new Sosi("유리"));
        sosiRepository.save(new Sosi("티파니"));
        sosiRepository.save(new Sosi("써니"));
        sosiRepository.save(new Sosi("서현"));
    }
}


6. 실행

소녀시대 정보 가져오기
http://localhost:8080/sosi/1

스케줄 추가하기
http://localhost:8080/schedule/add/1?program=무한도전

다시 소녀시대정보 가져오기
http://localhost:8080/sosi/1


예제 소스는 깃헙에...
https://github.com/mudchobo/sosi-schedule-sb

 
Posted by 머드초보
,
 
저번에 해커톤에 가서 코딩하고 온 할리갈리 게임입니다.
jQuery Mobile와 Java와 html5의 Cavans의 짬뽕 조화로.....이루어져있습니다....ㄷㄷ
Client는 Cavans로 만들었는데, IE계열에서 전혀 동작하지 않네요. IE9에서도 안되네요ㅠ 그리고 Android계열 브라우저에도 잘 안되는 듯 합니다. 되는 곳은 iPhone계열 밖에 없네요ㅠ 아이폰이랑은 같이 게임을 하실 수 있을겁니다^^
뭐 그냥 ChannelAPI 공부하는데에 전혀 무리 없이 보실 수 있을 것 같습니다.
물론 공식사이트에 있는 Tic-Tac-Toc 예제가 더 심플하고 보기 쉬운 예제입니다ㅠ 
http://code.google.com/p/java-channel-tic-tac-toe/

 그래도 그 외에 방 개념을 넣어서 만든거라 한서버당 여러 게임을 즐길 수 있어요!
근데 몰랐는데, 문제는 무료 계정을 사용할 경우 Resource에 한계가 있습니다. 근데, 이 ChannelAPI의 Channel Created 리소스는 너무 쪼잔하게 줍니다. 하루에 100개가 제한입니다. 결국은 접속자 100명이 들어오면 끝나는 것이죠....ㄷㄷ 페이지당 채널하나니 뭐 그냥 몇명이 게임하면 끝나는 것이죠 ㅇㅇ

 그래서 다음에는 Node.js로 만들어서 무료호스팅 하는 곳에 올려볼 생각입니다!
스샷

일단 원리는 간단합니다.

1. 해당 게임페이지에 접속 시 채널생성.

채널을 생성하고 Javascript에서 Open요청을 하면 /_ah/channel/connected 요청이 호출됩니다. 여기서 입장을 시키면 됩니다. 서버에 접속되었다고 객체의 유저목록에 추가하면 됩ㄴ니다. 그 뒤에 이 사용자가 끊어질 경우 /_ah/channel/disconnected 요청이 호출되므로 그때 유저목록에서 삭제해주면 되죠!

2. 해당 방에 객체를 만들어 ClientId저장

ClientId를 저장해두어야 나중에 해당 방에 있는 유저들에게만 데이터를 보낼 때 해당 ClientId로 보낼 수 있습니다. 

쓰다보니 별거없네... 암튼 이런식으로 되는거라......
 
소스는 여기에
https://github.com/mudchobo/HalliGalliForAppEngine 

올려놓은 사이트는 여기에
http://halligalligame.appspot.com/ 

PS. 만들면서 느끼는 게 Javascript로 타이머 하나 걸면 무조건 이기겠더라구요... 웹게임에 대한 어뷰저는 어떻게 처리를 해야할 지 고민이 크네요... 
 
Posted by 머드초보
,
 

이클립스에서 앱엔진플러그인으로 설치한 다음에 프로젝트를 만들고 테스트서버를 가동하면 해당PC로컬에서 밖에 접속이 안됩니다. 제가 모바일페이지를 만들고 있어서 폰에서도 테스트해보려고 열었더니 접속이 되지 않았습니다.
netstat -an을 때려보면 127.0.0.1의 8888포트로 접속을 연것을 볼 수 있습니다. 그렇다는 건 로컬에서밖에 접속이 안된다는 것이죠.
그래서 테스트할 땐 로컬에서 밖에 안되나....라고 검색해보니 역시나 방법이 있네요.

AppEngine에서 The Java Development Server를 보면 실행 시 옵션을 제공하는데요.
http://code.google.com/intl/ko-KR/appengine/docs/java/tools/devserver.html

--address=...
The host address to use for the server. You may need to set this to be able to access the development server from another computer on your network. An address of 0.0.0.0 allows both localhost access and hostname access. Default is localhost.

--address를 0.0.0.0값으로 해서 실행하면 된다는군요.

이클립스에서 Debug As로 가서 실행할 때 Arguments를 수정합니다.
--address=0.0.0.0 --port=8888 D:\mudchobo\workspace36\appengine\Mudchobo\war

이렇게 하면 됩니다.

모바일에서 접속하고 싶은 경우에는 공유기를 써서 접속했다면 같은 공유기에 접속을 하고 PC에서 ipconfig로 나오는 ip로 접속하면 됩니다~
 
Posted by 머드초보
,
 
아오...생각해보니까 안되는건데, 삽질하고 있었긔ㅠㅠ
안드로이드폰에서 서버를 구축하면 일단 3G에서는 외부에서 접속을 할 수 없어요. 안드로이드폰 웹브라우저에서만 접속할 수 있어요~
그래서 WI-FI모드에서만 가능하네요. WI-FI모드에서도 같은 망에 있어야지 가능하죠~
음....어차피 내가 만드려고 하는 것은 파일교환하는 거라서 뭐 WI-FI모드에서만 써도 될 듯...

일단 잡소리고-_-

Android에는 HttpClient와 HttpCore가 내장탑재되어있습니다. 이걸 이용하면 간단한 웹서버를 만들 수 있지요. 예제는 아래사이트에서...
확인할 수 있는데, 이상한 게 이거 그대로 쓰려고 하면 클래스가 몇 개 없어서 에러납니다.
이걸 보면 Android에 있는 HttpCore에 없는 클래스를 사용하더라구요. 그래서 Tutorial을 잘 보니까 이렇게 말고 다르게 써야하더라구요.
그래서 살짝 고쳐줘야합니다. 바뀐 부분만 보면-_-

RequestListenerThread 클래스의 생성자


public RequestListenerThread(int port, final String docroot) throws IOException {
        this.serversocket = new ServerSocket(port);
        
        this.params = new BasicHttpParams();
        this.params
            .setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
            .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024)
            .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
            .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
            .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "HttpComponents/1.1");

        BasicHttpProcessor httpproc = new BasicHttpProcessor();
        httpproc.addInterceptor(new ResponseDate());
        httpproc.addInterceptor(new ResponseServer());
        httpproc.addInterceptor(new ResponseContent());
        httpproc.addInterceptor(new ResponseConnControl());
        
        // Set up request handlers
        HttpRequestHandlerRegistry reqistry = new HttpRequestHandlerRegistry();
        reqistry.register("*", new HttpFileHandler(docroot));
        
        this.httpService = new HttpService(httpproc, new DefaultConnectionReuseStrategy(), new DefaultHttpResponseFactory());
        this.httpService.setHandlerResolver(reqistry);
    }
생성자 부분이 요렇게 바뀌어야합니다. 생각해보니....이거말고 고친게 없네요-_-
아마 잘 될겁니다 으핫.....

이건 풀소스-_-
PS. 티스토리는 개당 10메가만 안넘으면 계속 올릴 수 있어서 좋아~

 
Posted by 머드초보
,
 
기록용~!

아오.....안드로이드에다가 웹서버를 올려서 뭔가를 하려는 걸 구현하려고 하는데, 안드로이드에서는 com.sun.net.httpserver.HttpServer 이 클래스가 없네요ㅠㅠ 필요 없을 것 같아서 뺀 듯ㅠㅠ 전에 찾았던 소스도 다시 못찾겠고.....선호한테 물어봐야겠네....

암튼 초간단 서버 구축 클래스가 있었네요.

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import com.sun.net.httpserver.HttpServer;


public class Test {

	public static void main(String[] args) throws IOException {
		InetSocketAddress addr = new InetSocketAddress(9000);
		HttpServer server = HttpServer.create(addr, 0);
		
		server.createContext("/", new MyHandler());
		server.setExecutor(Executors.newCachedThreadPool());
		server.start();
	}
}

해당포트로 HttpServer.create로 서버 생성하고, 핸들러클래스를 파라메터로 넣으면 저기 핸들러에서 다 처리를 하는 듯하네요.

MyHandler.java

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;


public class MyHandler implements HttpHandler {

	private String root = "D:/root/";
	
	@Override
	public void handle(HttpExchange exchange) throws IOException {
		String requestMethod = exchange.getRequestMethod();
		if (requestMethod.equalsIgnoreCase("GET")){
			Headers responseHeaders = exchange.getResponseHeaders();
			responseHeaders.set("Content-Type", "text/html");
			URI uri = exchange.getRequestURI();
			System.out.println(uri.getPath());
			OutputStream responseBody = exchange.getResponseBody();
			BufferedReader br = new BufferedReader(new FileReader(root + uri.getPath()));
			exchange.sendResponseHeaders(200, 0);
			int b = 0;
			while((b = br.read()) != -1){
				responseBody.write(b);
			}
			responseBody.close();
		}
	}
}
get 요청인 경우 해당 path에서 파일을 찾아서 그냥 뿌려주는 게 다임 ㅇㅇ
지금셋팅은 http://localhost:9000/index.html을 요청하면  d:/root/index.html 파일을 읽어서 뿌려줌 ㅇㅇ

ps. 생각해보니 파일을 주고받으려고 찾고 있었는데, ftp서버를 찾았어야 했는데......젠장......ㅠㅠ
 
Posted by 머드초보
,