sort vs. sort_by
March 2nd, 2009
Enumerable - enum.sort_by {| obj | block } => array
Sorts enum using a set of keys generated by mapping the values in enum through the given block.
%w{ apple pear fig }.sort_by {|word| word.length}
#=> ["fig", "pear", "apple"]
The current implementation of sort_by generates an array of tuples containing the original collection element and the mapped value. This makes sort_by fairly expensive when the keysets are simple
require 'benchmark'
include Benchmark
a = (1..100000).map {rand(100000)}
bm(10) do |b|
b.report("Sort") { a.sort }
b.report("Sort by") { a.sort_by {|a| a} }
end
produces:
user system total real
Sort 0.180000 0.000000 0.180000 ( 0.175469)
Sort by 1.980000 0.040000 2.020000 ( 2.013586)
However, consider the case where comparing the keys is a non-trivial operation. The following code sorts some files on modification time using the basic sort method.
files = Dir["*"]
sorted = files.sort {|a,b| File.new(a).mtime <=> File.new(b).mtime}
sorted #=> ["mon", "tues", "wed", "thurs"]
This sort is inefficient: it generates two new File objects during every comparison. A slightly better technique is to use the Kernel#test method to generate the modification times directly.
files = Dir["*"]
sorted = files.sort { |a,b|
test(?M, a) <=> test(?M, b)
}
sorted #=> ["mon", "tues", "wed", "thurs"]
This still generates many unnecessary Time objects. A more efficient technique is to cache the sort keys (modification times in this case) before the sort. Perl users often call this approach a Schwartzian Transform, after Randal Schwartz. We construct a temporary array, where each element is an array containing our sort key along with the filename. We sort this array, and then extract the filename from the result.
sorted = Dir["*"].collect { |f|
[test(?M, f), f]
}.sort.collect { |f| f[1] }
sorted #=> ["mon", "tues", "wed", "thurs"]
This is exactly what sort_by does internally.
sorted = Dir["*"].sort_by {|f| test(?M, f)}
sorted #=> ["mon", "tues", "wed", "thurs"]
이런 글도 있다.
http://redcorundum.blogspot.com/2006/10/using-sortby-instead-of-sort.html
요약
Enumerable의 sort와 sort_by 는 둘 다 enum객체를 정렬해 주는 일을 한다. 근데 이 둘이 정렬하는 방법이 조금 다르다. sort_by는 정렬 대상이 되는 키들을 일단 한번 연산하여 테이블을 만들어 두고 나서 정렬하기 때문에 간단한 키셋인 경우 sort보다 좀 더 비싸진다.
그런데 sort_by 를 써야 할 상황이 존재한다. sort_by는 초기에 한번 정렬 대상값을 구해놓고 정렬을 하기 때문에 위 설명처럼 File 객체가 2nlogn 개만큼 생성된다든지, Time 객체가 2nlogn 개 만큼 생기거나 하지는 않는다. 이런 경우 sort_by가 sort보다 훨씬 좋은 벤치마크 결과를 내게 된다.
Schwartzian Transform이 멋지다. sort_by가 없었다면 루비스트들은 다 sort에 저 방식으로 정렬을 하고 있겠지? [정렬키, 아이템] 으로 묶은 후(collect) 정렬(sort)해서, 아이템만 취하는(collect) 방식이다. 펄 유저들은 이렇게 짜왔었나보다.
그리고 그 아래 링크는 여러 키를 조건으로 소트하는 방법에 sort_by를 이용하는 글이다. 역시 설명이 참 심플한데, sort에 Array로 인자를 넘기면 각 인자를 비교해 주니, 그걸 이용해서 내가 정렬하고 싶은 순서대로 키를 넘기는거다.
- some_collection.sort do |a, b|
- val = a.foo <=> b.boo
- val = a.bar <=> b.bar if val == 0
- val = a.baz <=> b.baz if val == 0
- val
- end
대신
-
some_collection.sort_by { |a| [a.foo, a.bar, a.baz] }
아하. 그렇구나.
ps. 루비 레퍼런스 설명도 너무 쉽게 잘 돼있어서 감동. ㅜㅜ
액티브 서포트 - core_ext/array*
September 27th, 2008
순서대로라면 callbacks.rb와 clean_logger.rb 를 봐야 할테지만 core_ext가 막 땡겨서 이거 먼저 보기로 했다.
core_ext는 이름에서도 알 수 있듯이 ruby 코어를 확장한 것이다. 루비의 클래스는 열려 있기 때문에 얼마든지 언어가 정의하고 있는 구현을 바꾸거나 추가할 수 있다.
첫번째는 정말 빠져서는 안될 자료구조, Array를 더 확장한다. 우왕국!
array.rb
ActiveSupport::CoreExtensions::Array::Access
Access 모듈은 from과 to를 구현한다. 이거, 진짜 자주 만들어내는 코든데-_-; 이런게 있었네?
이 코드에서는 저 간단한 코드보다도 명확한 주석이 눈에 확 들어온다. 아. 주석은 저렇게 다는거구나.
- # Returns the tail of the array from +position+.
#
# %w( a b c d ).from(0) # => %w( a b c d )
# %w( a b c d ).from(2) # => %w( c d )
# %w( a b c d ).from(10) # => nil
def from(position)
self[position..-1]
end
# Returns the beginning of the array up to +position+.
#
# %w( a b c d ).to(0) # => %w( a )
# %w( a b c d ).to(2) # => %w( a b c )
# %w( a b c d ).to(10) # => %w( a b c d )
def to(position)
self[0..position]
end
ActiveSupport::CoreExtensions::Array::Conversions
Conversions 모듈은 Array를 다른 것으로 바꾸는 일에 집중한다. to_sentence는 배열을 문자열로 만들어 주는데, 어떤 식으로 바꾸는 고 하니, %w(a b c)를 "a, b, and c" 로 바꾸는 식이다. 여기서 b 뒤의 ,를 빼느냐 마느냐의 옵션도 줄 수 있고, and 대신 뭘 쓸 것인가도 옵션으로 줄 수 있다. 물론 이것들은 전부 파라메터로.. 이 메서드는 코드는 좀 복잡해 보이지만, 역시 주석은 세줄로 끝난다. 주석은 이렇게 달아야 된다.-_-
- # Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
# * <tt>:connector</tt> - The word used to join the last element in arrays with two or more elements (default: "and")
# * <tt>:skip_last_comma</tt> - Set to true to return "a, b and c" instead of "a, b, and c".
def to_sentence(options = {})
options.assert_valid_keys(:connector, :skip_last_comma)
options.reverse_merge! :connector => 'and', :skip_last_comma => false
options[:connector] = "#{options[:connector]} " unless options[:connector].nil? || options[:connector].strip == '' - case length
when 0
""
when 1
self[0].to_s
when 2
"#{self[0]} #{options[:connector]}#{self[1]}"
else
"#{self[0...-1].join(', ')}#{options[:skip_last_comma] ? '' : ','} #{options[:connector]}#{self[-1]}"
end
end
assert_valid_keys(*valid_keys)는 core_ext/hash에서 확장된 메서드인데, hash가 가진 키 중에 valid_keys 안에 없는 키가 존재하면 ArgumentError를 발생시킨다. 즉, 여기서는 options에 :connector와 skip_last_comma 외의 다른 키가 존재하면 에러를 내는 일을 하는 코드가 첫 줄이다.
그 아래 reverse_merge! 는 똑같은 일을 merge를 이용해서 하려면 아래처럼 되어야 한다.
- options = {:connector => 'and', :skip_last_comma => false}.merge(options)
난 아직 이 코드가 더 눈에 익숙하게 들어오지만, options가 두 번 등장한다는 점에서 reverse_merge!가 좀 더 나을수도 있겠다고 세뇌해야겠다. dry dry ...
그 아래 살짝 내려가 self.included(base) 는 to_s를 재정의할 수 있도록 한다. 코드는 이런 식이다.
- def self.included(base) #:nodoc:
base.class_eval do
alias_method :to_default_s, :to_s
alias_method :to_s, :to_formatted_s
end
end
to_s를 to_default_s로 바꾸고, to_formatted_s를 to_s로 바꾼다. to_formatted_s는 바로 아래에 있다.
- # Converts a collection of elements into a formatted string by calling
# <tt>to_s</tt> on all elements and joining them:
#
# Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post"
#
# Adding in the <tt>:db</tt> argument as the format yields a prettier
# output:
#
# Blog.find(:all).to_formatted_s(:db) # => "First Post,Second Post,Third Post"
def to_formatted_s(format = :default)
case format
when :db
if respond_to?(:empty?) && self.empty?
"null"
else
collect { |element| element.id }.join(",")
end
else
to_default_s
end
end
역시 주석이 최고. :db 라는 인자를 주면 , 로 join하여 돌려준다. 없으면, 그냥 to_default_s를 쓴다.
결과적으로 보면 :db 라는 format을 구현하기 위해 이렇게 한 것일텐데, super 대신 메서드 이름을 바꾸는 것이 얼마나 장점이 있는가가 조금 의문으로 다가온다. 이 코드를 읽기 않은 레일스 개발자가 Array의 to_s를 잘 못 확장하면 무한루프에 빠지는 등의 문제가 생길 수도 있기 때문이다. 흠. 더 읽다 보면 이해가 되려나?
그 아래 to_xml도 있다. to_xml도 꽤 많은 일을 하는데, 이에 대한 것은 다음에 필요할 때 읽기로 하고 넘어감. (주석도 코드도 많다)
ActiveSupport::CoreExtensions::Array::ExtractOptions
ExtractOptions 모듈은 extract_options! 메서드 하나를 정의한다. 이 메서드는 Array 마지막 아이템이 해시일 경우 그것을 리턴하고, 아니면 빈 해시를 리턴한다. 이 메서드는 레일스 전체에서 48곳에서나!!(2.1.0 현재) 사용되고 있다. 레일스 메서드 인자 관례에서 그만큼 많이 사용되고 있는 것이다.
코드의 길이대비 사용 빈도가 정말 최고가 아닐까? 다른 메서드는 어떤지 궁금하고나. ㅎㅎ
- # Extracts options from a set of arguments. Removes and returns the last
# element in the array if it's a hash, otherwise returns a blank hash.
#
# def options(*args)
# args.extract_options!
# end
#
# options(1, 2) # => {}
# options(1, 2, :a => :b) # => {:a=>:b} - def extract_options!
last.is_a?(::Hash) ? pop : {}
end
ActiveSupport::CoreExtensions::Array::Grouping
Grouping 모듈은 in_groups_of(number, fill_with = nil, &block), split(value = nil, &block) 메서드를 제공한다.
in_groups_of 는 배열 아이템들을 number 씩 묶어 주는 것이다. 같은 개수로 파티셔닝한다고 설명하면 될 것 같다. 이렇게 하고, 혹시 빈 칸이 있다면 fill_with 로 채워준다. 기본값은 nil이므로 nil로 채워지고, false를 전달하면 빈칸을 만들지 않는다. 코드를 보자.
- # Iterates over the array in groups of size +number+, padding any remaining
# slots with +fill_with+ unless it is +false+.
#
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g}
# ["1", "2", "3"]
# ["4", "5", "6"]
# ["7", nil, nil]
#
# %w(1 2 3).in_groups_of(2, ' ') {|g| p g}
# ["1", "2"]
# ["3", " "]
#
# %w(1 2 3).in_groups_of(2, false) {|g| p g}
# ["1", "2"]
# ["3"]
def in_groups_of(number, fill_with = nil, &block)
if fill_with == false
collection = self
else
# size % number gives how many extra we have;
# subtracting from number gives how many to add;
# modulo number ensures we don't add group of just fill.
padding = (number - size % number) % number
collection = dup.concat([fill_with] * padding)
end
if block_given?
collection.each_slice(number, &block)
else
returning [] do |groups|
collection.each_slice(number) { |group| groups << group }
end
end
end
사실 array의 each_slice가 유사한 일을 한다. 여기서도 보면 알 수 있겠지만 fill_with가 false일 경우는 단순히 collection의, 즉 자기 자신의 each_slice 를 실행한다. each_slice는 padding을 뺀 모든 기능을 한다. 알고보면, in_groups_of는 each_slice 가 하는 일에 padding을 넣는 것만 더 하는 것이다. 흠. dup.과 concat은 잘 안 쓰는 메서드다. 게다가 약간 로직이 들어가는 padding을 만드는 부분을 위해 세 줄이나 주석을 달았다. 흠!
다음은 split(value = nil, &block) 이다. value 혹은 block이 주어짐에 따라 문자열 대신 배열을 split한다!
코드는 inject를 활용하였다. 좀 복잡하지만 한번 보자.
- # Divides the array into one or more subarrays based on a delimiting +value+
# or the result of an optional block.
#
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
def split(value = nil, &block)
block ||= Proc.new { |e| e == value }
inject([[]]) do |results, element|
if block.call(element)
results << []
else
results.last << element
end
results
end
end
첫 줄에서 만든 proc 혹은 블럭으로 넘어온 객체를 block.call(element) 해서 그 값이 참이면 split을 해야 하는 것이니 [[]] 에 []을 추가. 그러면 [[], []] 가 되고, 만약 block.call이 false가 되면 .last에 << element를 한다. 아항.
ActiveSupport::CoreExtensions::Array::RandomAccess
RandomAccess 에는 rand 메서드만 덜렁 하나 있다. 배열에서 아무 값이나 하나 임의로 추출하는 것이다. 특별한 것은 없음.
느낀점-_-
주석이 하는 일은 정말 대단하다. 전에 김기사님에게 이렇게 말했던 적이 있다. "코드가 무슨 일을 하는지 알고 보니 정말 잘 읽히더라." 처음엔 주석을 잘 써라라고 가르친다(입문서). 그 다음엔, 코드가 주석을 대신하라고 가르친다(리팩토링 등). 그 후, 내가 요즘 느끼는 것은 바로.. 코드는 구현을 잘 설명할 수 있도록 하고, 구현 외의 부분은 주석으로 설명해라.이다.
말처럼 쉽지는 않겠지만 노력하다 보면 또 내공이 올라가겠지.^^ 프로그래밍 누가 쉽다고 했던가.
액티브 서포트 - cache*
September 14th, 2008
코드를 읽어서 블로그에 포스팅을 한다 - 가 부담이 되서 코드를 안 읽고있다. -_-
그래서 그냥 코드만 읽고 대충대충 기록만 남기고 넘어가야 할 것 같다.
전에 말했던 대로 abc 순으로 읽어가고 있다.
그래서 오늘은 base64, basic_object, buffered_logger를 넘어
cache와, cache/ 안에 있는 여러 store들을 읽어보았다.
액티브서포트의 Cache 모듈은 Store 클래스를 가지고 있다. cache/ 안의 여러 파일들은 주로 Store를 재구현하는 코드들이다.
- DRbStore < MemoryStore < Store
- CompressedMemCacheStore < MemCacheStore < Store
- FileStore < Store
Store
Store는 거의 여러 Store들의 공통 코드들만을 정의하고 있다. read나 write도 log 남기는 것 외에 하는 일이 없다. 이 Store를 상속받는 것들을 하나씩 보자.
MemoryStore < Store
MemoryStore는 아주 전형적인 루비 상속 구조를 취하는 클래스다. 잠시만 보자면 -_-
memory_store.rb
- module ActiveSupport
module Cache
class MemoryStore < Store
def initialize
@data = {}
end
def read(name, options = nil)
super
@data[name]
end
def write(name, value, options = nil)
super
@data[name] = value
end
def delete(name, options = nil)
super
@data.delete(name)
end
def delete_matched(matcher, options = nil)
super
@data.delete_if { |k,v| k =~ matcher }
end
def exist?(name,options = nil)
super
@data.has_key?(name)
end
def clear
@data.clear
end
end
end
end
대략 한 줄 한 줄이 자바 코드로 1:1 매핑이 될 것 같은 느낌이 드는 코드다. (delete_if 부분만 빼고?)
전형적 루비 상속은 이렇게 하면 된다는 것을 보여준다.
drb_store.rb
- require 'drb'
module ActiveSupport
module Cache
class DRbStore < MemoryStore #:nodoc:
attr_reader :address
def initialize(address = 'druby://localhost:9192')
super()
@address = address
@data = DRbObject.new(nil, address)
end
end
end
end
별거 없다. druby 주소를 지정할 수 있는 address가 추가됐다. 오우.. 심플그자체. 코드는 작은데 많은 일을 한다.
mem_cache_store.rb
아마 우리가 자주 이야기하는 그 멤캐시를 쓰는 스토어인 것 같다. MemCacheStore의 생성자를 보면 잘 알 수 있다.
- def initialize(*addresses)
addresses = addresses.flatten
options = addresses.extract_options!
addresses = ["localhost"] if addresses.empty?
@addresses = addresses
@data = MemCache.new(addresses, options)
end
MemCache의 api를 이용해서 캐시한다. read는 단순히 MemCache 객체에 get 메세지를 보낸다.
write는 옵션에 따라 add 혹은 set 메세지를 보낸다.
- def read(key, options = nil)
super
@data.get(key, raw?(options))
rescue MemCache::MemCacheError => e
logger.error("MemCacheError (#{e}): #{e.message}")
nil
end - # Set key = value. Pass :unless_exist => true if you don't
# want to update the cache if the key is already set.
def write(key, value, options = nil)
super
method = options && options[:unless_exist] ? :add : :set
response = @data.send(method, key, value, expires_in(options), raw?(options))
response == Response::STORED
rescue MemCache::MemCacheError => e
logger.error("MemCacheError (#{e}): #{e.message}")
false
end
다른 메서드들도 대략 유사함.
compressed_mem_cache_store.rb
memcache를 사용하지만, 더 압축하고 싶을 때 쓰라고 만들어 둔 스토어다.
read와 write만 재구현했다. 쓸 때 압축하고, 읽을 때 압축을 푸는 것이다. 그뿐.
file_store.rb
마지막으로 FileStore다.
파일 스토어는 코드를 보면 알 수 있겠지만, cache_path를 지정하면 그 디렉토리를 캐시 디렉토리로 사용하고, 캐시 키 이름에 .cache를 붙여 파일로 바로 저장한다. 파일 이름을 만드는 로직은 private으로 선언돼 있다.
- def real_file_path(name)
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
end
Store 다시
cache.rb 안에 있는 Store를 다시 봤다. threadsafe! 란 메서드가 눈에 들어왔다.
- def threadsafe!
@mutex = Mutex.new
self.class.send :include, ThreadSafety
self
end
이 메서드를 호출하면 self 객체의 메타클래스에 ThreadSafety 가 포함? 된다라고 하면 될려나.
여튼 쉽게 이해하기로, 여러 Store 객체 중 이 객체만 ThreadSafety 모듈을 상속받게 됐다고 생각하면 되겠다.
그럼 ThreadSafety 모듈은 무슨 일을 하는가?
- module ThreadSafety #:nodoc:
def read(key, options = nil) #:nodoc:
@mutex.synchronize { super }
end
def write(key, value, options = nil) #:nodoc:
@mutex.synchronize { super }
end
def delete(key, options = nil) #:nodoc:
@mutex.synchronize { super }
end
def delete_matched(matcher, options = nil) #:nodoc:
@mutex.synchronize { super }
end
end
미리 정해둔 @mutex 객체를 사용해 크리티컬 섹션으로의 접근을 제어한다. 그 안에서는 단순히 super를 호출하여 원래 호출되어야 하는 메서드가 자연스럽게 호출된다.
머리속이 복잡하다.
ThreadSafety에서 super를 호출하면 어떤 클래스의 메서드가 호출되는 것인가? -_-;
ThreadSafety는 Store의 하위 클래스처럼 취급되는 것 같다. 하지만 동적으로 include 되기 때문에 예를들어 MemCacheStore 객체에게 threadsafe! 메세지를 보냈다면 그 객체를 구성하는 메타클래스로 올라가는 길에 ThreadSafety 모듈이 추가된다. 객체 이름이 store 라고 했을 때
- store < MemCacheStore
이 상태에서, threadsafe! 메세지를 보내면
- store < ThreadSafety < MemCacheStore
이렇게 되는 것 같다. 아.......그렇구나.
하지만 읽기만 할 뿐, 실제로 짜는 것은 아직 수련이 부족한것 같다. 하긴 그래서 이렇게 코드를 읽고 있는 것이기도 하징.
액티브 서포트 - b*.rb - buffered_logger.rb
September 2nd, 2008
요즘은 코코아, 루비, 그리고 jquery에 꽂혀 있다.
꽂혀만 있고 공부는 잘 안한다. ...
업무 중에 시간이 좀 남아서 일과 직접적으로 연관되는 jquery와 루비가 공부 후보로 올라왔는데
낮에 jquery를 처음 써보면서 공부를 약간 했었기 땜에 남는 저녁시간은 레일스를 보기로 했다.
액티브서포트부터 시작해서 내키는 데까지 코드를 쭉 훑어보기로 했다.
눈으로만 훑으면 나중에 다 까먹으니까 간단히 정리만 해 두기로 했다.
abc 순으로 제일 위에 있는 b*.rb 들을 봤다. a로 시작하는 파일은 없었다.
lib/buffered_logger.rb
- module ActiveSupport
# Inspired by the buffered logger idea by Ezra
class BufferedLogger
module Severity
DEBUG = 0
INFO = 1
WARN = 2
ERROR = 3
FATAL = 4
UNKNOWN = 5
end
include Severity - ...
class 안에 module 선언해서 enum처럼? 혹은 C의 define처럼? 활용한다. 오오.
- module ActiveSupport
# Inspired by the buffered logger idea by Ezra
class BufferedLogger - ...
- for severity in Severity.constants
class_eval <<-EOT, __FILE__, __LINE__
def #{severity.downcase}(message = nil, progname = nil, &block)
add(#{severity}, message, progname, &block)
end
def #{severity.downcase}?
#{severity} >= @level
end
EOT
end - ...
메서드 선언을 class_eval로 하는 코드.
logger.warn, logger.info, logger.warn? logger.info? 등의 메서드를 정의한다. 심플한 코드라 나중에 쉽게 참조하기 좋을듯.
- # Set the auto-flush period. Set to true to flush after every log message,
# to an integer to flush every N messages, or to false, nil, or zero to
# never auto-flush. If you turn auto-flushing off, be sure to regularly
# flush the log yourself -- it will eat up memory until you do.
def auto_flushing=(period)
@auto_flushing =
case period
when true; 1
when false, nil, 0; MAX_BUFFER_SIZE
when Integer; period
else raise ArgumentError, "Unrecognized auto_flushing period: #{period.inspect}"
end
end
auto_flushing= 은 period를 인자로 받는 것 같지만 boolean 값도 받아서 처리한다.
메서드는 한가지 일을 해야 하지만, 한 종류의 인자를 받을 필요는 없다. 하튼 자기 할 일만 잘하면 된다. 개발자 헛갈리지 않게.
이같은 코드는 생성자에도 존재하는데 바로 @log를 생성하는 부분.
- def initialize(log, level = DEBUG)
@level = level
@buffer = []
@auto_flushing = 1
@no_block = false
if log.respond_to?(:write)
@log = log
elsif File.exist?(log)
@log = open(log, (File::WRONLY | File::APPEND))
@log.sync = true
else
FileUtils.mkdir_p(File.dirname(log))
@log = open(log, (File::WRONLY | File::APPEND | File::CREAT))
@log.sync = true
@log.write("# Logfile created on %s" % [Time.now.to_s])
end
end
write가 있으면 그냥 쓴다. 왜냐면 우린 @log.write만 쓸 꺼니까..
그렇지 않으면 파일 이름으로 간주하고, 찾아본다. 그래도 없으면 디렉토리+파일 이름으로 간주하고 디렉토리도 만들고 찾는다.
파일이 없는 경우 이 로그 파일은 새 것이므로 맨 윗줄에 주석까지 달아주는 센스를 보여준다.
후에 write외에 다른 메서드를 쓸 일이 생기는데... 바로 close할 때다.
- def close
flush
@log.close if @log.respond_to?(:close)
@log = nil
end
@log가 IO 객체? 라면 아마 close를 모두 가지고 있을 것이다. 위위 코드에서 두번째와 세번째 경우는 모두 close를 가지고 있을 것이다.
하지만 직접 구현한 객체라면? 이 객체는 write는 있는데 close는 없다면?
그런 경우에 위 close 메서드의 두번째 줄 같은 코드가 필요하다.
duck type을 이용하여 @log를 생성하여 쓰고... close는 있으면 하고 없으면 말고.
아.. 아름답다. (라고 세뇌한다.-_-)
오늘은 비도 오고 하니까 이만
References
Xcode에서 GLUT와 OpenGL 프로젝트 만들기
April 13th, 2008
xcode는 맥os와 함께 제공되는 IDE입니다. 주로 코코아 어플리케이션을 만드는 데에 사용되지만, 비단 objc와 코코아 지원만 있는 것이 아니라, 모든 맥에서 사용하는 어플리케이션을 만드는 데 사용될 수 있는 툴인 것 같습니다. 현재 맥에서 가장 편하게 GLUT, OpenGL 어플리케이션을 만들 수 있는 게 xcode라고 판단이 되어서 좀 써보다가, 간단히 그 방법을 정리해서 올려 봅니다.
xcode 설치
일단 xcode를 설치해야 겠죠? xcode는 맥 os 가 들어있는 시디 두번째 장에 있었습니다. 저는 레퍼드가 들어있는 맥북프로 안에 있는 시디의 2번장이었는데, 좀 다를 수도 있겠죠? 여튼, xcode는 시디 안에 있고 웹에서도 직접 다운받을 수 있습니다. 제가 지금 설명하고 있는 버전은 3.0입니다.
클릭클릭 하면 잘 설치 됩니다.
다 설치하고 실행을 하면 아래와 같은 화면을 볼 수 있습니다.
프로젝트 생성
OpenGL 과 GLUT를 포함하는 프로젝트를 만들기 위해서 일단 프로젝트를 생성해야 겠죠.
File - New Project 메뉴를 선택하면 아래와 같은 화면이 등장합니다. 여기서 Command Line Utility 에서 원하는 언어를 골라 줍니다. 여기서는 C++ 툴을 만들겠습니다. 여기서 다른 항목을 선택하면 그 항목에 필요한 Framework들이 자동으로 프로젝트에 포함됩니다.
확인 버튼을 누르면 아래 화면이 나옵니다. 적당한 위치와 프로젝트 이름을 골라 Finish 버튼을 누릅니다. 저는 여기서 간단한 사각형을 그리는 프로젝트를 만들 것이므로 square라고 했습니다.
Framework 추가하기
그러면 방금 정한 이름으로 프로젝트가 만들어집니다. 방금 Command Line Utility 를 만들기로 했기 때문에, 특별히 포함된 프레임웍이 하나도 없는 것을 프로젝트 창에서 확인할 수 있을 것입니다. OpenGL 툴을 만들려면 필요한 프레임웍을 추가해야 합니다.
프로젝트 창 왼쪽 트리의 최상단 아이템을 보세요. 아마 프로젝트와 이름이 같은 항목이 있을 것입니다. 여기를 우클릭합니다. 그럼 아래와 같은 화면을 보실 수 있을 것입니다.
그림에 나와 있는 대로 Add - Existing Frameworks... 를 선택합니다. 우리는 OpenGL과 GLUT 프레임웍을 추가해야 합니다.
OpenGL.framework 와 GLUT.framework 를 찾아서 추가합니다. 추가할 때 아래와 같은 화면도 볼 수 있는데 특별히 변경하지 않아도 잘 동작하므로, Add 버튼을 눌러 추가합니다.
이렇게 두 개의 프레임웍을 추가하고 나면 아래와 같이 됩니다. 그러면 이제 진짜로 코딩에 들어갈 수 있게 됩니다.
사각형 그리기
일단 샘플 코드를 보시죠~
- #if !defined(__APPLE__)
#include <GL/glut.h>
#else
#include <GLUT/glut.h>
#endif
#include <iostream>
// http://dis.dankook.ac.kr/lectures/cg08/entry/Introductory-OpenGL-program
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_LINE_LOOP); // colored line-loop square
glColor3f(1.0, 0.0, 0.0); glVertex2f(-0.5, -0.5);
glColor3f(1.0, 1.0, 0.0); glVertex2f(-0.5, 0.5);
glColor3f(0.0, 1.0, 0.0); glVertex2f(0.5, 0.5);
glColor3f(1.0, 0.0, 1.0); glVertex2f(0.5, -0.5);
glEnd();
glFlush();
}
int main (int argc, char * argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glutInitWindowSize(200, 200);
glutInitWindowPosition(0,0);
glutCreateWindow(argv[0]);
glutDisplayFunc(display);
glutMainLoop();
}
여기서 제일 위 #if .. 항목을 봐 주세요. 보통은 GL 디렉토리에 GLUT 라이브러리까지 들어가는 편이지만, 애플에서 제공하는 GLUT.framework는 GLUT 라는 디렉토리에 glut.h 를 두고 있습니다. 따라서 저런 분기문이 필요합니다. 이렇게만 해 주면 오류없이 잘 컴파일 됩니다.
아, 그리고 main() 함수의 argv 파라메터가 처음에 char * const argv[] 형태로 선언되어 있는데 glutInit 함수가 argv를 const로 취급하지 않아서 컴파일시 에러가 발생합니다. glutInit을 바꾸거나 새로 const가 아닌 argv를 만들거나 해야 하는데 그보다 쉬운 방법은 그냥 main함수의 argv 변수 선언을 바꾸는 것입니다. const를 빼버리면 됩니다.
그리하여, 오류를 피해가는 코드가 위 샘플 코드입니다. 위 코드를 main.cpp 에 옮겨 보세요~~
위 코드를 cmd-b 로 빌드한 후에 cmd-return 으로 실행해 보면 아래와 같은 창이 뜰 것입니다. 그럼 모두 완료!